3 Commits
75 changed files with 17608 additions and 192 deletions
@@ -101,13 +101,14 @@ export function ObservationTimeline({
<Icon name="chevron-left" />
</Button>
<Button
className="observation-timeline__transport"
size="compact"
variant="secondary"
variant="ghost"
disabled={!buffered || !onPlayingChange}
aria-label={buffered ? (playing ? "Пауза" : "Воспроизвести") : "Только эфир"}
icon={<Icon name={playing ? "stop" : "play"} />}
onClick={() => onPlayingChange?.(!playing)}
>
{buffered ? (playing ? "Пауза" : "Воспроизвести") : "Только эфир"}
</Button>
/>
{buffered && playbackRate !== undefined && onPlaybackRateChange ? (
<Select
label="Скорость воспроизведения"
@@ -117,7 +118,7 @@ export function ObservationTimeline({
{ value: "1", label: "1×" },
{ value: "2", label: "2×" },
]}
variant="split"
variant="inline"
menuWidth="anchor"
onChange={(value) => onPlaybackRateChange(Number(value))}
/>
@@ -90,6 +90,22 @@ export function recordedMediaSegmentAppendOrder(
return missing;
}
export function recordedMediaCanRollTarget(
previousSequence: number,
nextSequence: number,
playing: boolean,
targetBuffered: boolean,
): boolean {
return Boolean(
playing
&& targetBuffered
&& Number.isInteger(previousSequence)
&& Number.isInteger(nextSequence)
&& previousSequence >= 1
&& nextSequence >= previousSequence
);
}
function recordedMediaTimeRangesContain(
ranges: TimeRanges,
targetSeconds: number,
@@ -1028,12 +1044,13 @@ export function RecordedFmp4Player({
forceReset: false,
resetAttempts: 0,
};
const rollingTarget = Boolean(
const rollingTarget = Boolean(previousTarget && recordedMediaCanRollTarget(
previousTarget.sequence,
candidateTarget.sequence,
playbackPlayingRef.current
&& previousTarget
&& runtime.notifiedRevision === previousTarget.revision
&& recordedSegmentTargetBuffered(runtime, candidateTarget),
);
&& runtime.notifiedRevision === previousTarget.revision,
recordedSegmentTargetBuffered(runtime, candidateTarget),
));
const reportPumpError = (error: unknown) => {
if (
runtime.disposed
@@ -32,6 +32,7 @@ export function LaboratoryEvidenceViewer<
transport,
trailingActions,
modeControlsVisible = true,
chromeLayout = "overlay",
children,
}: {
label: string;
@@ -52,6 +53,7 @@ export function LaboratoryEvidenceViewer<
transport?: ReactNode;
trailingActions?: ReactNode;
modeControlsVisible?: boolean;
chromeLayout?: "overlay" | "stacked";
children: ReactNode;
}) {
const expandButtonRef = useRef<HTMLButtonElement | null>(null);
@@ -80,6 +82,36 @@ export function LaboratoryEvidenceViewer<
return () => window.removeEventListener("keydown", onKeyDown);
}, [expanded, onExpandedChange]);
const controls = (
<div className="laboratory-evidence-viewer__controls">
{actions}
{modeControlsVisible && secondaryMode ? (
<SegmentedControl
value={secondaryMode.value}
items={[...secondaryMode.modes]}
label={secondaryMode.label}
onChange={secondaryMode.onChange}
/>
) : null}
{modeControlsVisible ? (
<SegmentedControl
value={mode}
items={[...modes]}
label={`${label}: режим представления`}
onChange={onModeChange}
/>
) : null}
{trailingActions}
<IconButton
ref={expandButtonRef}
label={expanded ? `Свернуть ${label}` : `Развернуть ${label}`}
onClick={() => onExpandedChange(!expanded)}
>
<Icon name={expanded ? "minimize" : "expand"} size={16} />
</IconButton>
</div>
);
const viewer = (
<dialog
ref={viewerRef}
@@ -90,44 +122,40 @@ export function LaboratoryEvidenceViewer<
].filter(Boolean).join(" ")}
data-expanded={expanded ? "true" : undefined}
data-mode-controls={modeControlsVisible ? undefined : "content"}
data-chrome-layout={chromeLayout}
aria-label={label}
>
<div className="laboratory-evidence-viewer__stage">
{children}
</div>
{overlay}
{transport ? (
<div className="laboratory-evidence-viewer__transport">
{transport}
</div>
) : null}
<div className="laboratory-evidence-viewer__controls">
{actions}
{modeControlsVisible && secondaryMode ? (
<SegmentedControl
value={secondaryMode.value}
items={[...secondaryMode.modes]}
label={secondaryMode.label}
onChange={secondaryMode.onChange}
/>
) : null}
{modeControlsVisible ? (
<SegmentedControl
value={mode}
items={[...modes]}
label={`${label}: режим представления`}
onChange={onModeChange}
/>
) : null}
{trailingActions}
<IconButton
ref={expandButtonRef}
label={expanded ? `Свернуть ${label}` : `Развернуть ${label}`}
onClick={() => onExpandedChange(!expanded)}
>
<Icon name={expanded ? "minimize" : "expand"} size={16} />
</IconButton>
</div>
{chromeLayout === "stacked" ? (
<>
<div className="laboratory-evidence-viewer__header">
<div className="laboratory-evidence-viewer__header-context">
{overlay}
</div>
{controls}
</div>
<div className="laboratory-evidence-viewer__stage">
{children}
</div>
{transport ? (
<div className="laboratory-evidence-viewer__transport">
{transport}
</div>
) : null}
</>
) : (
<>
<div className="laboratory-evidence-viewer__stage">
{children}
</div>
{overlay}
{transport ? (
<div className="laboratory-evidence-viewer__transport">
{transport}
</div>
) : null}
{controls}
</>
)}
</dialog>
);
@@ -40,6 +40,55 @@ export interface LaboratoryMetricCorridorVisual {
halfWidthM: number;
}
export interface LaboratoryMetricLegendEntry {
id: LaboratoryMetricDecision | "context" | "local-surface" | "rolling";
label: string;
}
export function laboratoryMetricLegendEntries({
pointCloudCount,
localSurfaceCount,
obstacles,
showCurrentIncrement,
showLocalSurface,
showRollingMap,
}: {
pointCloudCount: number;
localSurfaceCount: number;
obstacles: readonly LaboratoryMetricObstacleVisual[];
showCurrentIncrement: boolean;
showLocalSurface: boolean;
showRollingMap: boolean;
}): readonly LaboratoryMetricLegendEntry[] {
const visibleObstacles = obstacles.filter((obstacle) => (
obstacle.state === "current"
? showCurrentIncrement
: obstacle.state === "retained"
? showRollingMap
: false
));
const decisions = new Set(visibleObstacles.map(({ decision }) => decision));
const entries: LaboratoryMetricLegendEntry[] = [];
if (decisions.has("threat")) entries.push({ id: "threat", label: "Угроза" });
if (decisions.has("not-threat")) entries.push({ id: "not-threat", label: "Вне коридора" });
if (decisions.has("unknown")) entries.push({ id: "unknown", label: "Неизвестно" });
if (showCurrentIncrement && pointCloudCount > 0) {
entries.push({ id: "context", label: "Текущий кадр" });
}
if (showLocalSurface && localSurfaceCount > 0) {
entries.push({ id: "local-surface", label: "Локальная SLAM-поверхность" });
}
if (
showRollingMap
&& visibleObstacles.some((obstacle) => (
obstacle.state === "retained" && obstacle.cellCentersBodyXyzM.length > 0
))
) {
entries.push({ id: "rolling", label: "Занято на накопленной карте" });
}
return entries;
}
function tokenColor(
host: HTMLElement,
token: string,
@@ -167,7 +216,6 @@ LaboratoryMetricEvidenceSceneHandle,
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(tokenColor(host, "--nodedc-canvas", [5, 5, 6]), 1);
renderer.domElement.setAttribute("aria-label", label);
renderer.domElement.setAttribute("role", "img");
host.prepend(renderer.domElement);
@@ -221,6 +269,11 @@ LaboratoryMetricEvidenceSceneHandle,
staticContentRef.current = null;
dynamicContentRef.current = null;
};
}, []);
useEffect(() => {
const canvas = hostRef.current?.querySelector("canvas");
if (canvas) canvas.setAttribute("aria-label", label);
}, [label]);
useEffect(() => {
@@ -478,6 +531,14 @@ LaboratoryMetricEvidenceSceneHandle,
}];
});
})();
const metricLegendEntries = laboratoryMetricLegendEntries({
pointCloudCount: pointCloudBodyXyzM.length,
localSurfaceCount: localSurfaceBodyXyzM.length,
obstacles,
showCurrentIncrement,
showLocalSurface,
showRollingMap,
});
return (
<div className="laboratory-metric-evidence-scene">
@@ -485,12 +546,9 @@ LaboratoryMetricEvidenceSceneHandle,
{renderError ? <p>{renderError}</p> : null}
</div>
<div className="laboratory-metric-evidence-scene__legend">
<span data-decision="threat">Угроза</span>
<span data-decision="not-threat">Вне коридора</span>
<span data-decision="unknown">Неизвестно</span>
<span data-decision="context">Current increment</span>
<span data-decision="local-surface">Local SLAM surface</span>
<span data-decision="rolling">Rolling-map occupied</span>
{metricLegendEntries.map((entry) => (
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
))}
{semanticLegendEntries.map((entry) => (
<span
key={entry.id}
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { Select, StatusBadge } from "@nodedc/ui-react";
import { useId, useState, type ReactNode } from "react";
import { Icon, IconButton, Select, StatusBadge } from "@nodedc/ui-react";
export interface LaboratoryOption<T extends string> {
id: T;
@@ -146,78 +146,94 @@ export function LaboratorySummary({
method?: LaboratoryMethod | null;
}) {
const methodComplete = method?.completeness === "complete";
const [expanded, setExpanded] = useState(false);
const detailsId = useId();
return (
<section className="laboratory-summary">
<section className="laboratory-summary" data-expanded={expanded ? "true" : undefined}>
<header>
<div>
<div className="laboratory-summary__heading">
<span className="section-eyebrow">ЛАБОРАТОРНАЯ РАБОТА</span>
<h2>{title}</h2>
<p>{description}</p>
</div>
<StatusBadge tone={statusTone}>{status}</StatusBadge>
<div className="laboratory-summary__actions">
<StatusBadge tone={statusTone}>{status}</StatusBadge>
<IconButton
label={expanded ? "Свернуть подробности лабораторной работы" : "Раскрыть подробности лабораторной работы"}
aria-expanded={expanded}
aria-controls={detailsId}
onClick={() => setExpanded((current) => !current)}
>
<span className="laboratory-summary__toggle-glyph" aria-hidden="true">
<Icon name="chevron-down" />
</span>
</IconButton>
</div>
</header>
<dl className="laboratory-summary__facts">
{facts.map((fact) => (
<div key={fact.label}>
<dt>{fact.label}</dt>
<dd>{fact.value}</dd>
</div>
))}
</dl>
<dl className="laboratory-summary__brief">
<div>
<dt>Задача</dt>
<dd>{brief.question}</dd>
</div>
<div>
<dt>Как проверяли</dt>
<dd>{brief.approach}</dd>
</div>
<div>
<dt>Главный результат</dt>
<dd>{brief.principalResult}</dd>
</div>
<div>
<dt>Ограничение</dt>
<dd>{brief.limitation}</dd>
</div>
</dl>
{method ? (
<div className="laboratory-summary__method">
<header>
<div>
<span className="section-eyebrow">МЕТОД</span>
<strong>{method.pipelineId}</strong>
<div id={detailsId} className="laboratory-summary__details" hidden={!expanded}>
<p className="laboratory-summary__description">{description}</p>
<dl className="laboratory-summary__facts">
{facts.map((fact) => (
<div key={fact.label}>
<dt>{fact.label}</dt>
<dd>{fact.value}</dd>
</div>
<small>
{EXECUTION_LABELS[method.executionClass]}
{" · "}
{methodComplete ? "полная идентичность" : "legacy · частично"}
</small>
</header>
<dl className="laboratory-summary__components">
{method.components.map((component, index) => (
<div key={`${component.kind}:${component.name}:${index}`}>
<dt>{COMPONENT_LABELS[component.kind]}</dt>
<dd>
<strong>{component.name}</strong>
<small>
{component.role}
{" · "}
{component.version}
{component.identitySha256
? ` · ${component.identitySha256.slice(0, 12)}`
: ""}
</small>
</dd>
))}
</dl>
<dl className="laboratory-summary__brief">
<div>
<dt>Задача</dt>
<dd>{brief.question}</dd>
</div>
<div>
<dt>Как проверяли</dt>
<dd>{brief.approach}</dd>
</div>
<div>
<dt>Главный результат</dt>
<dd>{brief.principalResult}</dd>
</div>
<div>
<dt>Ограничение</dt>
<dd>{brief.limitation}</dd>
</div>
</dl>
{method ? (
<div className="laboratory-summary__method">
<header>
<div>
<span className="section-eyebrow">МЕТОД</span>
<strong>{method.pipelineId}</strong>
</div>
))}
</dl>
</div>
) : null}
<small>
{EXECUTION_LABELS[method.executionClass]}
{" · "}
{methodComplete ? "полная идентичность" : "legacy · частично"}
</small>
</header>
<dl className="laboratory-summary__components">
{method.components.map((component, index) => (
<div key={`${component.kind}:${component.name}:${index}`}>
<dt>{COMPONENT_LABELS[component.kind]}</dt>
<dd>
<strong>{component.name}</strong>
<small>
{component.role}
{" · "}
{component.version}
{component.identitySha256
? ` · ${component.identitySha256.slice(0, 12)}`
: ""}
</small>
</dd>
</div>
))}
</dl>
</div>
) : null}
</div>
</section>
);
}
@@ -0,0 +1,211 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
type RefObject,
} from "react";
import { SplitPane } from "@nodedc/ui-react";
import {
RecordedFmp4Player,
type RecordedObservationPlayback,
} from "../RecordedFmp4Player";
import { ObservationTimeline } from "../ObservationTimeline";
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
export const LABORATORY_RECORDED_CLIP_VIEWER_CONTRACT =
"missioncore.laboratory-recorded-clip-viewer/v1" as const;
export interface LaboratoryRecordedClipFrame {
sequence: number;
sourceTimeNs: number;
}
export function nearestLaboratoryRecordedClipFrame(
frames: readonly LaboratoryRecordedClipFrame[],
sourceTimeNs: number,
): LaboratoryRecordedClipFrame | null {
if (!frames.length || !Number.isFinite(sourceTimeNs)) return null;
let left = 0;
let right = frames.length - 1;
while (left < right) {
const middle = Math.floor((left + right) / 2);
if (frames[middle]!.sourceTimeNs < sourceTimeNs) left = middle + 1;
else right = middle;
}
const next = frames[left]!;
const previous = frames[Math.max(0, left - 1)]!;
return Math.abs(previous.sourceTimeNs - sourceTimeNs)
<= Math.abs(next.sourceTimeNs - sourceTimeNs)
? previous
: next;
}
export function laboratoryRecordedClipEndExclusiveNs(
frames: readonly LaboratoryRecordedClipFrame[],
): number | null {
const last = frames.at(-1);
if (!last) return null;
const deltas = frames.slice(1).flatMap((frame, index) => {
const delta = frame.sourceTimeNs - frames[index]!.sourceTimeNs;
return Number.isSafeInteger(delta) && delta > 0 ? [delta] : [];
}).sort((left, right) => left - right);
const typicalDelta = deltas.length
? deltas[Math.floor(deltas.length / 2)]!
: 100_000_000;
return last.sourceTimeNs + typicalDelta;
}
export function LaboratoryRecordedClipPlayer({
source,
segmentCount,
frames,
sequence,
playing,
playbackRate,
cameraPresentation,
continuousPlayback,
sourceCount,
cameraRef,
cameraOverlay,
alternativeScene,
onSequenceChange,
onPlayingChange,
onPlaybackRateChange,
}: {
source: ObservationSourceDescriptor;
segmentCount: number;
frames: readonly LaboratoryRecordedClipFrame[];
sequence: number;
playing: boolean;
playbackRate: number;
cameraPresentation: "primary" | "companion" | "hidden";
continuousPlayback: boolean;
sourceCount: number;
cameraRef?: RefObject<HTMLDivElement | null>;
cameraOverlay?: ReactNode;
alternativeScene?: ReactNode;
onSequenceChange: (sequence: number) => void;
onPlayingChange: (playing: boolean) => void;
onPlaybackRateChange: (rate: number) => void;
}) {
const [companionSpatialSize, setCompanionSpatialSize] = useState(69);
const lastEmittedSequenceRef = useRef(sequence);
lastEmittedSequenceRef.current = sequence;
const frame = useMemo(
() => frames.find((candidate) => candidate.sequence === sequence) ?? frames[0] ?? null,
[frames, sequence],
);
const endExclusiveNs = useMemo(
() => laboratoryRecordedClipEndExclusiveNs(frames),
[frames],
);
const playback = useMemo<RecordedObservationPlayback | null>(() => frame ? ({
currentSeconds: frame.sourceTimeNs / 1_000_000_000,
playing: continuousPlayback && playing,
rate: playbackRate,
}) : null, [continuousPlayback, frame, playbackRate, playing]);
useEffect(() => {
if (!continuousPlayback && playing) onPlayingChange(false);
}, [continuousPlayback, onPlayingChange, playing]);
const emitSequence = useCallback((nextSequence: number) => {
if (lastEmittedSequenceRef.current === nextSequence) return;
lastEmittedSequenceRef.current = nextSequence;
onSequenceChange(nextSequence);
}, [onSequenceChange]);
const handlePlaybackChange = useCallback((next: RecordedObservationPlayback) => {
const sourceTimeNs = Math.round(next.currentSeconds * 1_000_000_000);
const first = frames[0];
if (!first || endExclusiveNs === null) return;
if (sourceTimeNs >= endExclusiveNs) {
emitSequence(first.sequence);
return;
}
const nearest = nearestLaboratoryRecordedClipFrame(frames, sourceTimeNs);
if (nearest) emitSequence(nearest.sequence);
}, [emitSequence, endExclusiveNs, frames]);
const timelineStart = frames[0]?.sourceTimeNs ?? 0;
const timelineEnd = frames.at(-1)?.sourceTimeNs ?? timelineStart + 1;
const companionVisible = cameraPresentation === "companion";
const spatialSize = companionVisible
? companionSpatialSize
: cameraPresentation === "primary" ? 0 : 100;
const spatialPane = (
<div
className="laboratory-recorded-clip-player__spatial"
aria-hidden={cameraPresentation === "primary"}
>
{cameraPresentation !== "primary" ? alternativeScene : null}
</div>
);
const cameraPane = (
<div
ref={cameraRef}
className="laboratory-recorded-clip-player__camera"
aria-hidden={cameraPresentation === "hidden"}
>
{playback ? (
<RecordedFmp4Player
source={source}
playback={playback}
interactive={false}
prepare
segmentSequence={frame?.sequence}
segmentCount={segmentCount}
admissionKey={`${source.id}:${source.delivery?.id ?? "recorded"}`}
onPlaybackChange={handlePlaybackChange}
onPlayingRejected={() => onPlayingChange(false)}
/>
) : null}
{cameraPresentation !== "hidden" ? cameraOverlay : null}
</div>
);
return (
<div
className="laboratory-recorded-clip-player"
data-contract={LABORATORY_RECORDED_CLIP_VIEWER_CONTRACT}
data-camera-presentation={cameraPresentation}
>
<div className="laboratory-recorded-clip-player__stage">
<SplitPane
className="laboratory-recorded-clip-player__split"
primary={spatialPane}
secondary={cameraPane}
primarySize={spatialSize}
onPrimarySizeChange={setCompanionSpatialSize}
orientation="vertical"
minPrimarySize={companionVisible ? 24 : 0}
minSecondarySize={companionVisible ? 24 : 0}
resizable={companionVisible}
separatorLabel="Изменить размер 3D/ПЛАН и правой камеры"
/>
</div>
<ObservationTimeline
className="laboratory-recorded-clip-player__timeline"
active
sourceCount={sourceCount}
mode="recorded"
seekable
synchronization="frame-accurate"
rangeNs={{ min: timelineStart, max: Math.max(timelineEnd, timelineStart + 1) }}
currentNs={frame?.sourceTimeNs ?? timelineStart}
playing={continuousPlayback && playing}
playbackRate={continuousPlayback ? playbackRate : undefined}
onPlaybackRateChange={continuousPlayback ? onPlaybackRateChange : undefined}
onPlayingChange={continuousPlayback ? onPlayingChange : undefined}
onSeek={(timeNs) => {
const nearest = nearestLaboratoryRecordedClipFrame(frames, timeNs);
if (nearest) emitSequence(nearest.sequence);
}}
showJumpToEnd={false}
/>
</div>
);
}
@@ -0,0 +1,111 @@
import {
useEffect,
useRef,
type KeyboardEvent as ReactKeyboardEvent,
type ReactNode,
} from "react";
import { createPortal } from "react-dom";
const FOCUSABLE = [
"a[href]",
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
"[tabindex]:not([tabindex='-1'])",
].join(",");
function keepFocusInside(
event: ReactKeyboardEvent<HTMLElement>,
frame: HTMLElement,
): void {
if (event.key !== "Tab") return;
const focusable = Array.from(frame.querySelectorAll<HTMLElement>(FOCUSABLE));
if (!focusable.length) {
event.preventDefault();
frame.focus();
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
export function LaboratoryReviewWorkspaceFrame({
ariaLabel,
toolbar,
stage,
inspector,
overlays,
interactionEnabled = true,
returnFocusTarget,
onClose,
}: {
ariaLabel: string;
toolbar: ReactNode;
stage: ReactNode;
inspector?: ReactNode;
overlays?: ReactNode;
interactionEnabled?: boolean;
returnFocusTarget?: HTMLElement | null;
onClose: () => void;
}) {
const frameRef = useRef<HTMLElement>(null);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
useEffect(() => {
if (typeof document === "undefined") return;
const previousFocus = document.activeElement instanceof HTMLElement
? document.activeElement
: null;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const animationFrame = window.requestAnimationFrame(() => {
const firstFocusable = frameRef.current?.querySelector<HTMLElement>(FOCUSABLE);
(firstFocusable ?? frameRef.current)?.focus();
});
return () => {
window.cancelAnimationFrame(animationFrame);
document.body.style.overflow = previousOverflow;
const target = returnFocusTarget?.isConnected ? returnFocusTarget : previousFocus;
window.requestAnimationFrame(() => target?.focus());
};
}, [returnFocusTarget]);
if (typeof document === "undefined") return null;
return createPortal(
<section
ref={frameRef}
className="laboratory-review-workspace"
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
data-has-inspector={inspector ? "true" : undefined}
tabIndex={-1}
onKeyDown={(event) => {
if (!interactionEnabled) return;
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
onCloseRef.current();
return;
}
keepFocusInside(event, event.currentTarget);
}}
>
<header className="laboratory-review-workspace__toolbar">{toolbar}</header>
<div className="laboratory-review-workspace__stage">{stage}</div>
{inspector ? <footer className="laboratory-review-workspace__inspector">{inspector}</footer> : null}
{overlays}
</section>,
document.body,
);
}
@@ -38,8 +38,14 @@ import { fetchE46JRawFisheyeRealtime } from "./e46jRawFisheyeRealtime";
import { fetchE47SemanticSlamResult } from "./e47SemanticSlam";
import { fetchM4ThreatReplayResult } from "./m4ReplayThreat";
import { fetchM47ReferenceGraphLab } from "./m47ReferenceGraph";
import {
fetchM48LifecycleResult,
} from "./m48ObjectCentricQuality";
import { fetchM48SmallStaticRegression } from "./m48SmallStaticRegression";
export type AdvancedLaboratoryWorkId =
| "m48-object-centric-quality"
| "m48-small-static-passage-regression"
| "m47-reference-graph-shadow"
| "m4-replay-threat"
| "l3-pointpillars-visual-audit"
@@ -82,6 +88,8 @@ export interface AdvancedLaboratoryIndexItem {
}
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"m48-object-centric-quality",
"m48-small-static-passage-regression",
"m47-reference-graph-shadow",
"m4-replay-threat",
"l3-pointpillars-visual-audit",
@@ -119,6 +127,8 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
];
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
"m48-small-static-passage-regression": "m48-small-static-passage-regression",
"m47-reference-graph-shadow": "m47-reference-graph-lab",
"m4-replay-threat": "m4-threat-replay",
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
@@ -164,6 +174,8 @@ export function isAdvancedLaboratoryWorkId(
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
return {
m47Graph: null,
m48: null,
m48SmallStatic: null,
m4Threat: null,
l3: null,
l31: null,
@@ -288,7 +300,9 @@ export function advancedLaboratoryResultAvailable(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
): boolean {
return workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
return workId === "m48-object-centric-quality" ? results.m48 !== null
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
: workId === "m4-replay-threat" ? results.m4Threat !== null
: workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
: workId === "l31-pointpillars-ravnoves" ? results.l31 !== null
@@ -337,7 +351,13 @@ export async function fetchAdvancedLaboratoryResult(
} = {},
): Promise<AdvancedLaboratoryResults> {
const results = emptyAdvancedLaboratoryResults();
if (workId === "m47-reference-graph-shadow") {
if (workId === "m48-object-centric-quality") {
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8 lifecycle evidence identity не выбрана.");
results.m48 = await fetchM48LifecycleResult(resultId, { fetcher, signal });
} else if (workId === "m48-small-static-passage-regression") {
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R1 regression identity не выбрана.");
results.m48SmallStatic = await fetchM48SmallStaticRegression(resultId, { fetcher, signal });
} else if (workId === "m47-reference-graph-shadow") {
if (!resultId) {
throw new AdvancedLaboratoryContractError("M4.7 LAB identity не выбрана.");
}
@@ -34,9 +34,13 @@ import type { E46JRawFisheyeRealtimeResult } from "./e46jRawFisheyeRealtime";
import type { E47SemanticSlamResult } from "./e47SemanticSlam";
import type { M4ThreatReplayResult } from "./m4ReplayThreat";
import type { M47ReferenceGraphLabResult } from "./m47ReferenceGraph";
import type { M48AdvancedResult } from "./m48ObjectCentricQuality";
import type { M48SmallStaticRegressionResult } from "./m48SmallStaticRegression";
export interface AdvancedLaboratoryResults {
m47Graph: M47ReferenceGraphLabResult | null;
m48: M48AdvancedResult | null;
m48SmallStatic: M48SmallStaticRegressionResult | null;
m4Threat: M4ThreatReplayResult | null;
l3: L3PointPillarsVisualAuditResult | null;
l31: L31PointPillarsRavnovesResult | null;
@@ -967,7 +967,7 @@ export async function fetchAdvancedLaboratoryResults({
const e39 = settledCatalogValue(settled[7]);
const e40 = settledCatalogValue(settled[8]);
return {
m47Graph: null, m4Threat: null,
m47Graph: null, m48: null, m48SmallStatic: null, m4Threat: null,
l3: null, l31: null,
l32: null,
l33: null,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,364 @@
import type { LaboratoryFetch } from "./advancedResults";
import type {
M48Authority,
M48Freshness,
M48GeometryAssociation,
M48Motion,
M48Threat,
M48Visibility,
} from "./m48ObjectCentricQuality";
const RESULT_ID = /^m48-small-static-passage-regression-[a-f0-9]{64}$/;
const PACK_ID = /^m48-object-quality-pack-[a-f0-9]{64}$/;
const ANCHOR_ID = /^anchor-[a-f0-9]{24}$/;
const SAFE_URL = /^\/api\/v1\/laboratory\/m48\/[A-Za-z0-9/_?&=.%:-]+$/;
export interface M48SmallStaticRegressionMetrics {
assistedAnchorCount: number;
assistedTrackletCount: number;
anchorClipCount: number;
requiresAvoidanceOrClearanceCount: number;
workerRecalledAnchorCount: number;
workerMissedAnchorCount: number;
assistedAnchorRecall: number;
extentIouThreshold: number;
minimumAssistedAnchorRecall: number;
}
export interface M48SmallStaticRegressionResult {
resultId: string;
packId: string;
createdAtUtc: string;
runLabel: "M4.8R1";
pipelineId: "m48-class-free-object-quality/v1";
experimentId: "m48-small-static-passage-regression/v1";
accepted: boolean;
metrics: M48SmallStaticRegressionMetrics;
gates: {
anchorSetNonEmpty: boolean;
developmentAnchorRecallTarget: boolean;
independentTruthAvailable: false;
};
decision: {
state: "accepted-development-regression-baseline" | "failed-development-regression-baseline";
summary: string;
nextAction: string;
};
groundTruth: false;
independentTruth: false;
authority: M48Authority;
}
export interface M48SmallStaticRegressionCaseSummary {
anchorId: string;
clipId: string;
sequence: number;
requiresAvoidanceOrClearance: boolean;
workerCandidateCount: number;
bestIou: number;
matchedAtThreshold: boolean;
outcome: "recalled" | "missed-assisted-anchor";
}
export interface M48SmallStaticRegressionObject {
predictionId: string;
extentXyxy: readonly [number, number, number, number];
geometryAssociation: M48GeometryAssociation;
freshness: M48Freshness;
motion: M48Motion;
threat: M48Threat;
}
export interface M48SmallStaticRegressionCase {
resultId: string;
packId: string;
anchor: {
anchorId: string;
clipId: string;
objectId: string;
sequence: number;
extentXyxy: readonly [number, number, number, number];
visibility: M48Visibility;
geometryAssociation: M48GeometryAssociation;
freshness: M48Freshness;
motion: M48Motion;
threat: M48Threat;
requiresAvoidanceOrClearance: boolean;
};
comparison: M48SmallStaticRegressionCaseSummary & {
sourceTimeNs: number;
anchorExtentXyxy: readonly [number, number, number, number];
workerObjects: readonly M48SmallStaticRegressionObject[];
bestPredictionId: string | null;
extentIouThreshold: number;
};
cameraUrl: string | null;
spatialUrl: string | null;
groundTruth: false;
authority: M48Authority;
}
export class M48SmallStaticRegressionContractError extends Error {}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new M48SmallStaticRegressionContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function arrayValue(value: unknown, label: string): readonly unknown[] {
if (!Array.isArray(value)) {
throw new M48SmallStaticRegressionContractError(`${label}: ожидался список.`);
}
return value;
}
function exact(value: unknown, expected: string | boolean, label: string): void {
if (value !== expected) {
throw new M48SmallStaticRegressionContractError(`${label}: нарушен контракт.`);
}
}
function textValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new M48SmallStaticRegressionContractError(`${label}: ожидалась строка.`);
}
return value;
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new M48SmallStaticRegressionContractError(`${label}: ожидалось число.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
const result = numberValue(value, label);
if (!Number.isInteger(result) || result < 0) {
throw new M48SmallStaticRegressionContractError(`${label}: ожидалось целое число.`);
}
return result;
}
function booleanValue(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new M48SmallStaticRegressionContractError(`${label}: ожидался флаг.`);
}
return value;
}
function extentValue(
value: unknown,
label: string,
): readonly [number, number, number, number] {
const rows = arrayValue(value, label).map((item, index) => numberValue(item, `${label}[${index}]`));
if (
rows.length !== 4
|| rows.some((item) => item < 0 || item > 1)
|| rows[0]! >= rows[2]!
|| rows[1]! >= rows[3]!
) {
throw new M48SmallStaticRegressionContractError(`${label}: рамка недопустима.`);
}
return rows as unknown as readonly [number, number, number, number];
}
function authorityValue(value: unknown): M48Authority {
const authority = objectValue(value, "M4.8R1.authority");
exact(authority.mode, "replay-simulated", "M4.8R1.authority.mode");
exact(authority.physical_live, false, "M4.8R1.authority.physical_live");
exact(authority.commands_enabled, false, "M4.8R1.authority.commands_enabled");
exact(authority.actuation_allowed, false, "M4.8R1.authority.actuation_allowed");
exact(authority.navigation_or_safety_accepted, false, "M4.8R1.authority.navigation_or_safety_accepted");
return {
mode: "replay-simulated",
physicalLive: false,
commandsEnabled: false,
actuationAllowed: false,
navigationOrSafetyAccepted: false,
};
}
function enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {
if (typeof value !== "string" || !allowed.includes(value as T)) {
throw new M48SmallStaticRegressionContractError(`${label}: неизвестное значение.`);
}
return value as T;
}
function parseSummary(value: unknown): M48SmallStaticRegressionCaseSummary {
const row = objectValue(value, "M4.8R1.case");
const anchorId = textValue(row.anchor_id, "M4.8R1.case.anchor_id");
if (!ANCHOR_ID.test(anchorId)) {
throw new M48SmallStaticRegressionContractError("M4.8R1.case.anchor_id: нарушена идентичность.");
}
return {
anchorId,
clipId: textValue(row.clip_id, "M4.8R1.case.clip_id"),
sequence: integerValue(row.sequence, "M4.8R1.case.sequence"),
requiresAvoidanceOrClearance: booleanValue(row.requires_avoidance_or_clearance, "M4.8R1.case.requires_avoidance_or_clearance"),
workerCandidateCount: integerValue(row.worker_candidate_count, "M4.8R1.case.worker_candidate_count"),
bestIou: numberValue(row.best_iou, "M4.8R1.case.best_iou"),
matchedAtThreshold: booleanValue(row.matched_at_threshold, "M4.8R1.case.matched_at_threshold"),
outcome: enumValue(row.outcome, ["recalled", "missed-assisted-anchor"] as const, "M4.8R1.case.outcome"),
};
}
export async function fetchM48SmallStaticRegression(
resultId: string,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<M48SmallStaticRegressionResult> {
if (!RESULT_ID.test(resultId)) {
throw new M48SmallStaticRegressionContractError("M4.8R1 result identity недопустима.");
}
const response = await fetcher(`/api/v1/laboratory/m48/regressions/small-static/${encodeURIComponent(resultId)}`, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) throw new M48SmallStaticRegressionContractError(`M4.8R1 недоступен: HTTP ${response.status}.`);
const payload = objectValue(await response.json(), "M4.8R1");
exact(payload.schema_version, "missioncore.m48-small-static-passage-regression-result-view/v1", "M4.8R1.schema_version");
const packId = textValue(payload.pack_id, "M4.8R1.pack_id");
if (!PACK_ID.test(packId)) throw new M48SmallStaticRegressionContractError("M4.8R1.pack_id: нарушена идентичность.");
const metrics = objectValue(payload.metrics, "M4.8R1.metrics");
const gates = objectValue(payload.gates, "M4.8R1.gates");
const decision = objectValue(payload.decision, "M4.8R1.decision");
exact(payload.run_label, "M4.8R1", "M4.8R1.run_label");
exact(payload.pipeline_id, "m48-class-free-object-quality/v1", "M4.8R1.pipeline_id");
exact(payload.experiment_id, "m48-small-static-passage-regression/v1", "M4.8R1.experiment_id");
exact(payload.ground_truth, false, "M4.8R1.ground_truth");
exact(payload.independent_truth, false, "M4.8R1.independent_truth");
exact(gates.independent_truth_available, false, "M4.8R1.gates.independent_truth_available");
return {
resultId,
packId,
createdAtUtc: textValue(payload.created_at_utc, "M4.8R1.created_at_utc"),
runLabel: "M4.8R1",
pipelineId: "m48-class-free-object-quality/v1",
experimentId: "m48-small-static-passage-regression/v1",
accepted: booleanValue(payload.accepted, "M4.8R1.accepted"),
metrics: {
assistedAnchorCount: integerValue(metrics.assisted_anchor_count, "M4.8R1.metrics.assisted_anchor_count"),
assistedTrackletCount: integerValue(metrics.assisted_tracklet_count, "M4.8R1.metrics.assisted_tracklet_count"),
anchorClipCount: integerValue(metrics.anchor_clip_count, "M4.8R1.metrics.anchor_clip_count"),
requiresAvoidanceOrClearanceCount: integerValue(metrics.requires_avoidance_or_clearance_count, "M4.8R1.metrics.requires_avoidance_or_clearance_count"),
workerRecalledAnchorCount: integerValue(metrics.worker_recalled_anchor_count, "M4.8R1.metrics.worker_recalled_anchor_count"),
workerMissedAnchorCount: integerValue(metrics.worker_missed_anchor_count, "M4.8R1.metrics.worker_missed_anchor_count"),
assistedAnchorRecall: numberValue(metrics.assisted_anchor_recall, "M4.8R1.metrics.assisted_anchor_recall"),
extentIouThreshold: numberValue(metrics.extent_iou_threshold, "M4.8R1.metrics.extent_iou_threshold"),
minimumAssistedAnchorRecall: numberValue(metrics.minimum_assisted_anchor_recall, "M4.8R1.metrics.minimum_assisted_anchor_recall"),
},
gates: {
anchorSetNonEmpty: booleanValue(gates.anchor_set_non_empty, "M4.8R1.gates.anchor_set_non_empty"),
developmentAnchorRecallTarget: booleanValue(gates.development_anchor_recall_target, "M4.8R1.gates.development_anchor_recall_target"),
independentTruthAvailable: false,
},
decision: {
state: enumValue(decision.state, ["accepted-development-regression-baseline", "failed-development-regression-baseline"] as const, "M4.8R1.decision.state"),
summary: textValue(decision.summary, "M4.8R1.decision.summary"),
nextAction: textValue(decision.next_action, "M4.8R1.decision.next_action"),
},
groundTruth: false,
independentTruth: false,
authority: authorityValue(payload.authority),
};
}
export async function fetchM48SmallStaticRegressionCases(
resultId: string,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<readonly M48SmallStaticRegressionCaseSummary[]> {
if (!RESULT_ID.test(resultId)) throw new M48SmallStaticRegressionContractError("M4.8R1 result identity недопустима.");
const response = await fetcher(`/api/v1/laboratory/m48/regressions/small-static/${encodeURIComponent(resultId)}/cases`, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) throw new M48SmallStaticRegressionContractError(`M4.8R1 cases недоступны: HTTP ${response.status}.`);
const payload = objectValue(await response.json(), "M4.8R1 cases");
exact(payload.schema_version, "missioncore.m48-small-static-passage-regression-case-catalog/v1", "M4.8R1 cases.schema_version");
const cases = arrayValue(payload.cases, "M4.8R1 cases.items").map(parseSummary);
if (integerValue(payload.case_count, "M4.8R1 cases.case_count") !== cases.length) {
throw new M48SmallStaticRegressionContractError("M4.8R1 cases: размер изменился.");
}
exact(payload.ground_truth, false, "M4.8R1 cases.ground_truth");
authorityValue(payload.authority);
return cases;
}
export async function fetchM48SmallStaticRegressionCase(
resultId: string,
anchorId: string,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<M48SmallStaticRegressionCase> {
if (!RESULT_ID.test(resultId) || !ANCHOR_ID.test(anchorId)) {
throw new M48SmallStaticRegressionContractError("M4.8R1 case identity недопустима.");
}
const response = await fetcher(`/api/v1/laboratory/m48/regressions/small-static/${encodeURIComponent(resultId)}/cases/${encodeURIComponent(anchorId)}`, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) throw new M48SmallStaticRegressionContractError(`M4.8R1 case недоступен: HTTP ${response.status}.`);
const payload = objectValue(await response.json(), "M4.8R1 case");
exact(payload.schema_version, "missioncore.m48-small-static-passage-regression-case-view/v1", "M4.8R1 case.schema_version");
const anchor = objectValue(payload.anchor, "M4.8R1 case.anchor");
const comparison = objectValue(payload.comparison, "M4.8R1 case.comparison");
const summary = parseSummary(comparison);
const workerObjects = arrayValue(comparison.worker_objects, "M4.8R1 case.worker_objects").map((raw) => {
const object = objectValue(raw, "M4.8R1 worker object");
return {
predictionId: textValue(object.prediction_id, "M4.8R1 worker object.prediction_id"),
extentXyxy: extentValue(object.extent_xyxy, "M4.8R1 worker object.extent_xyxy"),
geometryAssociation: enumValue(object.geometry_association, ["associated", "unavailable", "ineligible", "unknown"] as const, "M4.8R1 worker object.geometry_association"),
freshness: enumValue(object.freshness, ["current", "held", "stale", "unavailable"] as const, "M4.8R1 worker object.freshness"),
motion: enumValue(object.motion, ["moving", "static", "unknown", "unsupported"] as const, "M4.8R1 worker object.motion"),
threat: enumValue(object.threat, ["threat", "not-threat", "unknown"] as const, "M4.8R1 worker object.threat"),
};
});
const cameraUrl = payload.camera_url === null ? null : textValue(payload.camera_url, "M4.8R1 case.camera_url");
const spatialUrl = payload.spatial_url === null ? null : textValue(payload.spatial_url, "M4.8R1 case.spatial_url");
if ((cameraUrl && !SAFE_URL.test(cameraUrl)) || (spatialUrl && !SAFE_URL.test(spatialUrl))) {
throw new M48SmallStaticRegressionContractError("M4.8R1 case evidence URL недопустим.");
}
exact(payload.ground_truth, false, "M4.8R1 case.ground_truth");
const packId = textValue(payload.pack_id, "M4.8R1 case.pack_id");
if (!PACK_ID.test(packId)) {
throw new M48SmallStaticRegressionContractError("M4.8R1 case.pack_id: нарушена идентичность.");
}
if (textValue(anchor.anchor_id, "M4.8R1 case.anchor.anchor_id") !== anchorId) {
throw new M48SmallStaticRegressionContractError("M4.8R1 case.anchor_id: ответ не соответствует запросу.");
}
return {
resultId,
packId,
anchor: {
anchorId: textValue(anchor.anchor_id, "M4.8R1 case.anchor.anchor_id"),
clipId: textValue(anchor.clip_id, "M4.8R1 case.anchor.clip_id"),
objectId: textValue(anchor.object_id, "M4.8R1 case.anchor.object_id"),
sequence: integerValue(anchor.sequence, "M4.8R1 case.anchor.sequence"),
extentXyxy: extentValue(anchor.extent_xyxy, "M4.8R1 case.anchor.extent_xyxy"),
visibility: enumValue(anchor.visibility, ["visible", "partial", "occluded"] as const, "M4.8R1 case.anchor.visibility"),
geometryAssociation: enumValue(anchor.geometry_association, ["associated", "unavailable", "ineligible", "unknown"] as const, "M4.8R1 case.anchor.geometry_association"),
freshness: enumValue(anchor.freshness, ["current", "held", "stale", "unavailable"] as const, "M4.8R1 case.anchor.freshness"),
motion: enumValue(anchor.motion, ["moving", "static", "unknown", "unsupported"] as const, "M4.8R1 case.anchor.motion"),
threat: enumValue(anchor.threat, ["threat", "not-threat", "unknown"] as const, "M4.8R1 case.anchor.threat"),
requiresAvoidanceOrClearance: booleanValue(anchor.requires_avoidance_or_clearance, "M4.8R1 case.anchor.requires_avoidance_or_clearance"),
},
comparison: {
...summary,
sourceTimeNs: integerValue(comparison.source_time_ns, "M4.8R1 case.comparison.source_time_ns"),
anchorExtentXyxy: extentValue(comparison.anchor_extent_xyxy, "M4.8R1 case.comparison.anchor_extent_xyxy"),
workerObjects,
bestPredictionId: comparison.best_prediction_id === null ? null : textValue(comparison.best_prediction_id, "M4.8R1 case.comparison.best_prediction_id"),
extentIouThreshold: numberValue(comparison.extent_iou_threshold, "M4.8R1 case.comparison.extent_iou_threshold"),
},
cameraUrl,
spatialUrl,
groundTruth: false,
authority: authorityValue(payload.authority),
};
}
+4
View File
@@ -3,6 +3,9 @@
@import "./styles/shell.css";
@import "./styles/workspaces.css";
@import "./styles/laboratory.css";
@import "./styles/laboratory-evidence-viewer.css";
@import "./styles/laboratory-recorded-clip-player.css";
@import "./styles/laboratory-review-workspace.css";
@import "./styles/e40-case-review.css";
@import "./styles/l3-pointpillars-visual-audit.css";
@import "./styles/l34-annotation.css";
@@ -10,6 +13,7 @@
@import "./styles/laboratory-evidence-report.css";
@import "./styles/e34-temporal-layer.css";
@import "./styles/m4-replay-threat.css";
@import "./styles/m48-object-centric-quality.css";
@import "./styles/e35-degradation-recovery.css";
@import "./styles/e30-human-review.css";
@import "./styles/spatial.css";
@@ -0,0 +1,46 @@
.laboratory-evidence-viewer[data-chrome-layout="stacked"] {
display: grid;
box-sizing: border-box;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 0;
background: var(--nodedc-canvas);
}
.laboratory-evidence-viewer[data-chrome-layout="stacked"]
.laboratory-evidence-viewer__header {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
border: 0;
border-radius: 0;
background: var(--nodedc-canvas);
padding: 0.45rem 0.55rem;
}
.laboratory-evidence-viewer[data-chrome-layout="stacked"]
.laboratory-evidence-viewer__header-context {
min-width: 0;
flex: 1;
}
.laboratory-evidence-viewer[data-chrome-layout="stacked"]
.laboratory-evidence-viewer__controls {
position: static;
z-index: auto;
flex: none;
}
.laboratory-evidence-viewer[data-chrome-layout="stacked"]
.laboratory-evidence-viewer__stage {
position: relative;
inset: auto;
overflow: hidden;
}
.laboratory-evidence-viewer[data-chrome-layout="stacked"]
.laboratory-evidence-viewer__transport {
position: relative;
inset: auto;
}
@@ -0,0 +1,94 @@
.laboratory-recorded-clip-player {
display: grid;
width: 100%;
height: 100%;
min-height: 0;
grid-template-rows: minmax(0, 1fr) auto;
background: var(--nodedc-canvas);
}
.laboratory-recorded-clip-player__stage,
.laboratory-recorded-clip-player__split,
.laboratory-recorded-clip-player__spatial,
.laboratory-recorded-clip-player__camera {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.laboratory-recorded-clip-player__stage {
display: block;
}
.laboratory-recorded-clip-player__split,
.laboratory-recorded-clip-player__split > .nodedc-split-pane__panel,
.laboratory-recorded-clip-player__spatial,
.laboratory-recorded-clip-player__camera {
width: 100%;
height: 100%;
}
.laboratory-recorded-clip-player__camera {
pointer-events: auto;
}
.laboratory-recorded-clip-player__split
> .nodedc-split-pane__separator::before {
background: transparent;
box-shadow: none;
}
.laboratory-recorded-clip-player__split
> .nodedc-split-pane__separator:hover::before,
.laboratory-recorded-clip-player__split
> .nodedc-split-pane__separator:focus-visible::before,
.laboratory-recorded-clip-player__split[data-dragging="true"]
> .nodedc-split-pane__separator::before {
background: rgb(var(--nodedc-accent-rgb));
box-shadow: 0 0 0 2px rgb(var(--nodedc-accent-rgb) / 0.12);
}
.laboratory-recorded-clip-player__camera > .recorded-media-player {
position: absolute;
inset: 0;
}
.laboratory-recorded-clip-player
> .laboratory-recorded-clip-player__timeline.observation-timeline {
border: 0;
border-radius: 0;
background: var(--nodedc-canvas);
padding: 0.45rem 0.7rem;
backdrop-filter: none;
}
.laboratory-recorded-clip-player
> .laboratory-recorded-clip-player__timeline
.observation-timeline__playback {
grid-template-columns: auto auto auto minmax(12rem, 1fr) auto;
}
.m48-atlas-visual[data-chrome-layout="stacked"]
.laboratory-recorded-clip-player {
gap: 0;
background: transparent;
}
.m48-atlas-visual[data-chrome-layout="stacked"]
.laboratory-recorded-clip-player__stage,
.m48-atlas-visual[data-chrome-layout="stacked"]
.laboratory-recorded-clip-player > .observation-timeline {
box-sizing: border-box;
border: 0;
border-radius: 0;
background: var(--nodedc-canvas);
}
@media (max-width: 760px) {
.laboratory-recorded-clip-player
> .laboratory-recorded-clip-player__timeline
.observation-timeline__playback {
grid-template-columns: auto minmax(0, 1fr) auto;
}
}
@@ -1,3 +1,32 @@
.laboratory-summary > header {
align-items: center;
}
.laboratory-summary__heading {
min-width: 0;
}
.laboratory-summary__actions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: var(--nodedc-space-3);
}
.laboratory-summary__toggle-glyph {
display: inline-grid;
place-items: center;
transition: transform var(--nodedc-duration-fast) var(--nodedc-ease-standard);
}
.laboratory-summary[data-expanded="true"] .laboratory-summary__toggle-glyph {
transform: rotate(180deg);
}
.laboratory-summary__details[hidden] {
display: none;
}
.laboratory-summary .laboratory-summary__brief {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -0,0 +1,44 @@
.laboratory-review-workspace {
position: fixed;
z-index: var(--nodedc-layer-overlay);
inset: 0;
display: grid;
min-width: 0;
min-height: 0;
grid-template-rows: auto minmax(0, 1fr);
background: var(--nodedc-canvas);
color: var(--nodedc-text-primary);
}
.laboratory-review-workspace[data-has-inspector="true"] {
grid-template-rows: auto minmax(0, 1fr) auto;
}
.laboratory-review-workspace__toolbar,
.laboratory-review-workspace__inspector {
z-index: 5;
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
background: var(--nodedc-floating-surface);
padding: 0.55rem 0.7rem;
backdrop-filter: blur(var(--nodedc-blur-control));
}
.laboratory-review-workspace__stage {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
background: var(--nodedc-canvas);
}
@media (max-width: 1100px) {
.laboratory-review-workspace__toolbar,
.laboratory-review-workspace__inspector {
align-items: stretch;
flex-direction: column;
}
}
@@ -93,11 +93,14 @@
.laboratory-summary > header,
.laboratory-result-summary > header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1.5rem;
}
.laboratory-result-summary > header {
align-items: flex-start;
}
.laboratory-summary h2,
.laboratory-summary p,
.laboratory-summary dl,
@@ -115,10 +118,10 @@
letter-spacing: -0.025em;
}
.laboratory-summary > header p,
.laboratory-summary__description,
.laboratory-result-summary > p {
max-width: 66rem;
margin-top: 0.38rem;
margin-top: 0.65rem;
color: var(--nodedc-text-muted);
font-size: 0.63rem;
line-height: 1.55;
@@ -0,0 +1,370 @@
.m48-review-workspace__header {
display: grid;
width: 100%;
min-width: 0;
gap: var(--nodedc-space-3);
}
.m48-review-workspace__topbar,
.m48-review-workspace__topbar-start,
.m48-review-workspace__topbar-end,
.m48-review-workspace__clip-navigation,
.m48-review-workspace__source-heading,
.m48-review-workspace__evidence-summary,
.m48-review-workspace__object-tools {
display: flex;
min-width: 0;
align-items: center;
gap: 0.45rem;
}
.m48-review-workspace__topbar {
display: grid;
width: 100%;
grid-template-columns: minmax(0, 1fr) auto;
align-items: flex-end;
}
.m48-review-workspace__topbar-start,
.m48-review-workspace__topbar-end {
align-items: flex-end;
}
.m48-review-workspace__topbar-start {
overflow: hidden;
}
.m48-review-workspace__clip-navigation {
flex: 0 0 auto;
}
.m48-review-workspace__clip-field {
width: clamp(15rem, 19vw, 19rem);
flex: 0 1 19rem;
}
.m48-review-workspace__topbar-end {
flex: 0 0 auto;
justify-content: flex-end;
justify-self: end;
}
.m48-review-workspace__clip-reviewed {
width: clamp(14rem, 16vw, 20rem);
flex: 0 1 clamp(14rem, 16vw, 20rem);
}
.m48-review-workspace__state,
.m48-atlas-visual__state,
.m48-clip-player__state {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
gap: 0.55rem;
color: var(--nodedc-text-secondary);
}
.m48-review-workspace__evidence-summary {
flex-wrap: wrap;
color: var(--nodedc-text-secondary);
font-size: 0.68rem;
}
.m48-review-workspace__object-tools {
align-items: flex-end;
justify-content: flex-start;
overflow-x: auto;
padding-bottom: 0.1rem;
}
.m48-review-workspace__object-field {
width: 8rem;
min-width: 7.25rem;
max-width: 9.25rem;
flex: 1 1 8rem;
}
.m48-review-workspace__object-field--wide {
width: 11.5rem;
min-width: 10.5rem;
max-width: 13rem;
flex-basis: 11.5rem;
}
.m48-review-workspace__passage-field {
width: clamp(15rem, 18vw, 18rem);
min-width: 15rem;
flex: 1 1 15rem;
max-width: 18rem;
}
.m48-review-workspace__passage-field > .nodedc-checker {
width: 100%;
}
.m48-review-workspace__stage-shell {
position: relative;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.m48-evidence-stage {
position: relative;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.m48-evidence-stage > .laboratory-recorded-clip-player {
width: 100%;
height: 100%;
}
.m48-evidence-mode-rail {
position: absolute;
z-index: 7;
top: 50%;
left: var(--nodedc-space-4);
transform: translateY(-50%);
}
.m48-review-workspace__source-sticker {
position: absolute;
z-index: 6;
top: 3.15rem;
left: 0.75rem;
display: grid;
width: min(31rem, calc(100% - 1.5rem));
gap: 0.25rem;
pointer-events: none;
}
.m48-review-workspace__source-sticker small {
color: var(--nodedc-text-muted);
font-size: 0.58rem;
}
.m48-review-workspace__source-sticker strong {
font-size: 0.68rem;
}
.m48-review-workspace__freeze-form {
display: grid;
gap: 1rem;
}
.m48-clip-player__overlay {
position: absolute;
z-index: 2;
inset: 0;
width: 100%;
height: 100%;
touch-action: none;
}
.m48-clip-player__spatial-pane {
position: relative;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.m48-clip-player__spatial-pane > .laboratory-metric-evidence-scene {
width: 100%;
height: 100%;
}
.m48-clip-player__pane-label {
position: absolute;
z-index: 4;
top: 0.6rem;
border: 0;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-floating-surface);
padding: 0.38rem 0.52rem;
color: var(--nodedc-text-secondary);
font-size: 0.52rem;
font-weight: 700;
letter-spacing: 0.04em;
pointer-events: none;
backdrop-filter: blur(var(--nodedc-blur-control));
}
.m48-clip-player__pane-label[data-pane="spatial"] {
left: 0.6rem;
}
.m48-clip-player__pane-label[data-pane="camera"] {
right: 0.6rem;
}
.laboratory-recorded-clip-player[data-camera-presentation="companion"]
.m48-clip-player__pane-label[data-pane="camera"] {
top: 4.35rem;
}
.m48-clip-player__overlay[data-drawing="true"] {
cursor: crosshair;
}
.m48-clip-player__overlay g rect,
.m48-clip-player__draft-box {
fill: transparent;
stroke: rgb(var(--nodedc-accent-rgb));
stroke-width: 2;
vector-effect: non-scaling-stroke;
}
.m48-clip-player__overlay g[data-selected="true"] rect {
stroke-width: 3;
}
.m48-clip-player__overlay:not([data-drawing="true"]) g rect {
cursor: move;
}
.m48-clip-player__resize-handle {
fill: rgb(var(--nodedc-accent-rgb));
stroke: var(--nodedc-canvas);
stroke-width: 2;
vector-effect: non-scaling-stroke;
}
.m48-clip-player__resize-handle[data-handle="nw"],
.m48-clip-player__resize-handle[data-handle="se"] {
cursor: nwse-resize;
}
.m48-clip-player__resize-handle[data-handle="ne"],
.m48-clip-player__resize-handle[data-handle="sw"] {
cursor: nesw-resize;
}
.m48-clip-player__draft-box {
stroke-dasharray: 7 5;
}
.m48-clip-player__overlay text {
fill: var(--nodedc-text-primary);
font-size: 11px;
font-weight: 700;
paint-order: stroke;
stroke: var(--nodedc-canvas);
stroke-width: 3;
vector-effect: non-scaling-stroke;
}
.m48-atlas-visual__scene {
position: relative;
min-width: 0;
min-height: 0;
}
.m48-evidence-mode-controls {
display: flex;
min-width: 0;
align-items: center;
flex-direction: column;
gap: var(--nodedc-space-2);
}
.m48-evidence-mode-controls__text {
font-size: var(--nodedc-font-size-xs);
font-weight: var(--nodedc-font-weight-strong);
line-height: 1;
}
.m48-atlas-visual__scene {
width: 100%;
height: 100%;
overflow: hidden;
}
.m48-atlas-visual__scene > img,
.m48-atlas-visual__scene > canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.m48-atlas-visual__scene > img {
object-fit: contain;
}
.m48-atlas-visual__case {
display: grid;
max-width: min(34rem, 65%);
gap: 0.15rem;
border-radius: var(--nodedc-radius-control);
background: var(--nodedc-floating-surface);
padding: 0.55rem 0.7rem;
backdrop-filter: blur(var(--nodedc-blur-control));
}
.m48-atlas-visual[data-chrome-layout="stacked"]
.laboratory-evidence-viewer__header-context
.m48-atlas-visual__case {
max-width: 38rem;
background: transparent;
padding: 0 0.15rem;
backdrop-filter: none;
}
.m48-atlas-visual__case strong {
font-size: 0.65rem;
}
.m48-atlas-visual__case small {
overflow: hidden;
color: var(--nodedc-text-muted);
font-size: 0.52rem;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 1100px) {
.m48-review-workspace__topbar {
grid-template-columns: minmax(0, 1fr) auto;
align-items: end;
}
.m48-review-workspace__topbar-end {
grid-column: 2;
grid-row: 1;
}
.m48-review-workspace__clip-field {
width: min(100%, 19rem);
}
.m48-review-workspace__topbar-start,
.m48-review-workspace__object-tools {
flex-wrap: wrap;
}
.m48-review-workspace__clip-reviewed,
.m48-review-workspace__passage-field {
width: min(100%, 28rem);
flex-basis: min(100%, 28rem);
}
.m48-atlas-visual[data-chrome-layout="stacked"]
.laboratory-evidence-viewer__header {
align-items: stretch;
flex-direction: column;
}
.m48-atlas-visual[data-chrome-layout="stacked"]
.laboratory-evidence-viewer__controls {
width: 100%;
justify-content: flex-end;
}
}
@@ -400,10 +400,17 @@ i[data-availability="error"] {
}
.observation-timeline__playback > .nodedc-select-anchor {
width: 7.5rem;
display: inline-flex;
width: auto;
flex: none;
}
.observation-timeline__transport {
width: var(--nodedc-control-height-compact);
padding: 0;
border-radius: var(--nodedc-radius-circle);
}
.observation-timeline__accumulation {
display: grid;
min-width: 0;
@@ -42,6 +42,8 @@ import { E46JRawFisheyeRealtimeResultView } from "./E46JRawFisheyeRealtimeResult
import { E47SemanticSlamResultView } from "./E47SemanticSlamResult";
import { M4ReplayThreatResultView } from "./M4ReplayThreatResult";
import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult";
import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
export { isAdvancedLaboratoryWorkId };
export type { AdvancedLaboratoryWorkId };
@@ -84,6 +86,12 @@ export function AdvancedLaboratoryResult({
failedSessionId: string | null;
replayError: string | null;
}) {
if (workId === "m48-object-centric-quality" && results.m48) {
return <M48ObjectCentricQualityResultView rigLabel={rigLabel} result={results.m48} />;
}
if (workId === "m48-small-static-passage-regression" && results.m48SmallStatic) {
return <M48SmallStaticPassageRegressionResultView rigLabel={rigLabel} result={results.m48SmallStatic} />;
}
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
}
@@ -48,6 +48,7 @@ import { useLaboratoryValueReviewIndex } from "./useLaboratoryValueReviewIndex";
import { useLaboratoryEvidenceReport } from "./useLaboratoryEvidenceReport";
import { useLaboratoryViewMode } from "./useLaboratoryViewMode";
import { useL34AnnotationCapability } from "./annotation/useL34AnnotationCapability";
import { useM48ReviewCapability } from "./annotation/useM48ReviewCapability";
import {
buildLaboratoryCatalog,
buildLaboratoryProfiles,
@@ -526,6 +527,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
});
const advancedResults: AdvancedLaboratoryResults = advanced.results;
const annotationWorkspace = useL34AnnotationCapability({ selectedWorkId: viewMode === "laboratory" ? workId : "", l34Result: advancedResults.l34, l34dResult: advancedResults.l34d, l34eResult: advancedResults.l34e, e46Result: advancedResults.e46, e46aResult: advancedResults.e46a, onActionChange: props.onLaboratoryAnnotationActionChange });
const m48Review = useM48ReviewCapability({ selectedWorkId: viewMode === "laboratory" ? workId : "", initialGate: advancedResults.m48?.kind === "review" ? advancedResults.m48 : null, onActionChange: props.onLaboratoryAnnotationActionChange });
useEffect(() => {
const controller = new AbortController();
setEvidenceLoading(true);
@@ -825,7 +827,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
onChange={selectWork}
/>
<div className="laboratory-work-output">
{!m48Review.active ? <div className="laboratory-work-output">
{workId === "e28-local-surface" ? (
<LaboratoryWorkTemplate
summary={(
@@ -947,8 +949,9 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
<p>Неподтверждённый результат скрыт из лабораторного каталога.</p>
</div>
)}
</div>
</div> : null}
{annotationWorkspace}
{m48Review.workspace}
</div>
);
}
@@ -0,0 +1,203 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
RecordedEvidenceBoxOverlay,
type RecordedEvidenceBox,
} from "../../components/laboratory/RecordedEvidenceBoxOverlay";
import {
fetchM48FailureAtlas,
fetchM48FailureCase,
fetchM48ReviewSourceCatalog,
type M48FailureCase,
type M48FailureCaseSummary,
} from "../../core/laboratory/m48ObjectCentricQuality";
import {
M48BlindClipPlayer,
type M48BlindEvidenceMode,
} from "./annotation/M48BlindClipPlayer";
import { useM48SpatialClipPlayback } from "./annotation/useM48SpatialClipPlayback";
import { M48EvidenceModeRail } from "./annotation/M48EvidenceModeControls";
const ATLAS_MODES = [
{ value: "source", label: "SOURCE" },
{ value: "truth", label: "TRUTH" },
{ value: "graph", label: "GRAPH" },
{ value: "overlay", label: "OVERLAY" },
] as const;
type AtlasMode = typeof ATLAS_MODES[number]["value"];
function message(error: unknown): string {
return error instanceof Error && error.message.trim() ? error.message : "M4.8 evidence недоступно.";
}
function FailureAtlasScene({ item, mode }: { item: M48FailureCase; mode: AtlasMode }) {
const [size, setSize] = useState({ width: 1440, height: 1080 });
const boxes = useMemo<RecordedEvidenceBox[]>(() => {
const truth = mode === "truth" || mode === "overlay"
? item.truth.map((object) => ({
boxXyxy: [object.extentXyxy[0] * size.width, object.extentXyxy[1] * size.height, object.extentXyxy[2] * size.width, object.extentXyxy[3] * size.height] as const,
label: `truth · ${object.objectId}`,
tone: "success" as const,
}))
: [];
const graph = mode === "graph" || mode === "overlay"
? item.graph.map((object) => ({
boxXyxy: [object.extentXyxy[0] * size.width, object.extentXyxy[1] * size.height, object.extentXyxy[2] * size.width, object.extentXyxy[3] * size.height] as const,
label: `graph · ${object.objectId}`,
tone: "danger" as const,
dashed: mode === "overlay",
}))
: [];
return [...truth, ...graph];
}, [item.graph, item.truth, mode, size.height, size.width]);
if (!item.frame.cameraUrl) {
return <div className="m48-atlas-visual__state" role="alert"><Icon name="alert" size={18} />Точный camera-кадр failure case недоступен.</div>;
}
return (
<div className="m48-atlas-visual__scene">
<img
src={item.frame.cameraUrl}
alt=""
draggable={false}
onLoad={(event) => setSize({
width: event.currentTarget.naturalWidth || 1440,
height: event.currentTarget.naturalHeight || 1080,
})}
/>
<RecordedEvidenceBoxOverlay imageWidth={size.width} imageHeight={size.height} boxes={boxes} ariaLabel="M4.8 truth/graph failure overlay" />
</div>
);
}
export function M48FailureAtlasVisual({ resultId }: { resultId: string }) {
const [cases, setCases] = useState<readonly M48FailureCaseSummary[]>([]);
const [index, setIndex] = useState(0);
const [item, setItem] = useState<M48FailureCase | null>(null);
const [mode, setMode] = useState<AtlasMode>("overlay");
const [expanded, setExpanded] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
void fetchM48FailureAtlas(resultId, { signal: controller.signal })
.then((next) => {
if (!controller.signal.aborted) setCases(next);
})
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
.finally(() => !controller.signal.aborted && setLoading(false));
return () => controller.abort();
}, [resultId]);
useEffect(() => {
const selected = cases[index];
if (!selected) {
setItem(null);
return;
}
const controller = new AbortController();
setLoading(true);
void fetchM48FailureCase(resultId, selected.caseId, { signal: controller.signal })
.then((next) => !controller.signal.aborted && setItem(next))
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
.finally(() => !controller.signal.aborted && setLoading(false));
return () => controller.abort();
}, [cases, index, resultId]);
return (
<LaboratoryEvidenceViewer
label="M4.8 failure atlas"
className="m48-atlas-visual"
mode={mode}
modes={ATLAS_MODES}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<>
<IconButton label="Предыдущий failure case" disabled={!cases.length} onClick={() => setIndex((current) => (current - 1 + cases.length) % cases.length)}><Icon name="chevron-left" size={16} /></IconButton>
<IconButton label="Следующий failure case" disabled={!cases.length} onClick={() => setIndex((current) => (current + 1) % cases.length)}><Icon name="chevron-right" size={16} /></IconButton>
</>
)}
overlay={item ? <div className="m48-atlas-visual__case"><StatusBadge tone={item.split === "development" ? "neutral" : item.severity === "critical" ? "danger" : "warning"}>{item.split.toUpperCase()} · {item.severity}</StatusBadge><strong>{item.clipId} · frame {item.sequence}</strong><small>{item.split === "development" ? "Diagnostic only · " : "Validation acceptance evidence · "}{item.failures.join(" · ")}</small></div> : null}
>
{loading ? <div className="m48-atlas-visual__state" role="status"><span className="busy-indicator" aria-hidden="true" />Загружаем bounded failure case</div>
: error ? <div className="m48-atlas-visual__state" role="alert"><Icon name="alert" size={18} />{error}</div>
: item ? <FailureAtlasScene item={item} mode={mode} />
: <div className="m48-atlas-visual__state" role="status"><Icon name="check" size={18} />Failure atlas пуст: ни один bounded failure case не зафиксирован.</div>}
</LaboratoryEvidenceViewer>
);
}
export function M48ReviewPackVisual({ packId }: { packId: string }) {
const [catalog, setCatalog] = useState<Awaited<ReturnType<typeof fetchM48ReviewSourceCatalog>> | null>(null);
const [clipIndex, setClipIndex] = useState(0);
const [sequence, setSequence] = useState(1);
const [mode, setMode] = useState<M48BlindEvidenceMode>("camera");
const [cameraVisible, setCameraVisible] = useState(true);
const [expanded, setExpanded] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
void fetchM48ReviewSourceCatalog(packId, { signal: controller.signal })
.then((next) => {
if (controller.signal.aborted) return;
setCatalog(next);
if (next.clips[0]) setSequence(next.clips[0].startSequence);
})
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)));
return () => controller.abort();
}, [packId]);
const clip = catalog?.clips[clipIndex] ?? null;
const spatialEnabled = Boolean(catalog?.evidenceCapabilities.currentPointCloudBodyXyzM && catalog.evidenceCapabilities.rig && catalog.evidenceCapabilities.virtualCorridor);
const {
frame: spatial,
loading: spatialLoading,
error: spatialError,
} = useM48SpatialClipPlayback({
packId,
clip,
sequence,
enabled: mode !== "camera" && spatialEnabled,
});
return (
<LaboratoryEvidenceViewer
label="M4.8 neutral review pack"
className="m48-atlas-visual"
mode={mode}
modes={[]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
modeControlsVisible={false}
chromeLayout="stacked"
actions={<><IconButton label="Предыдущий клип" disabled={!catalog} onClick={() => {
if (!catalog) return;
const next = (clipIndex - 1 + catalog.clips.length) % catalog.clips.length;
setClipIndex(next);
setSequence(catalog.clips[next]!.startSequence);
}}><Icon name="chevron-left" size={16} /></IconButton><IconButton label="Следующий клип" disabled={!catalog} onClick={() => {
if (!catalog) return;
const next = (clipIndex + 1) % catalog.clips.length;
setClipIndex(next);
setSequence(catalog.clips[next]!.startSequence);
}}><Icon name="chevron-right" size={16} /></IconButton></>}
overlay={clip ? <div className="m48-atlas-visual__case"><StatusBadge tone="neutral">SOURCE ONLY</StatusBadge><strong>{clip.ordinal}/{catalog?.clipCount} · {clip.clipId}</strong><small>0 classes · 0 candidate identity · 0 model predictions</small></div> : null}
>
<div className="m48-evidence-stage">
{error ? <div className="m48-atlas-visual__state" role="alert"><Icon name="alert" size={18} />{error}</div>
: clip && catalog?.cameraPlayback ? <M48BlindClipPlayer cameraPlayback={catalog.cameraPlayback} clip={clip} sequence={sequence} mode={mode} cameraVisible={cameraVisible} tracklets={[]} selectedObjectId={null} editable={false} drawing={false} spatialFrame={spatial} spatialLoading={spatialLoading} spatialError={spatialError} spatialEvidenceAvailable={spatialEnabled} onSequenceChange={setSequence} onDrawingChange={() => undefined} onSelectedObjectIdChange={() => undefined} onTrackletsChange={() => undefined} />
: catalog ? <div className="m48-atlas-visual__state" role="alert"><Icon name="alert" size={18} />Pack-bound camera playback недоступен.</div>
: <div className="m48-atlas-visual__state" role="status"><span className="busy-indicator" aria-hidden="true" />Загружаем source-only clip pack</div>}
{catalog ? <M48EvidenceModeRail mode={mode} cameraVisible={cameraVisible} spatialAvailable={spatialEnabled} onModeChange={setMode} onCameraVisibleChange={setCameraVisible} /> : null}
</div>
</LaboratoryEvidenceViewer>
);
}
@@ -0,0 +1,165 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type {
M48AdvancedResult,
M48GateStatus,
M48QualityResult,
} from "../../core/laboratory/m48ObjectCentricQuality";
import { M48FailureAtlasVisual, M48ReviewPackVisual } from "./M48FailureAtlasVisual";
function percent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
function reviewStatus(result: M48GateStatus): { text: string; tone: "neutral" | "warning" | "success" } {
if (result.evaluated) return { text: "Evaluation завершена · откройте финальный M4.8 result", tone: "success" };
if (result.correctionState === "frozen") return { text: "24/24 клипов проверено · assisted evidence Worker 006 зафиксировано", tone: "success" };
if (result.correctionState !== "not-started") return { text: `${result.correctionReviewedClipCount}/${result.clipCount} клипов проверено · исправляем авторазметку Worker 006`, tone: "warning" };
if (result.adjudicationFrozen) return { text: "Truth seal зафиксирован · готово к evaluation", tone: "success" };
if (result.adjudicationUnlocked) return { text: "2/2 независимых review · открыта adjudication", tone: "warning" };
if (result.frozenReviewerCount > 0) return { text: `${result.frozenReviewerCount}/2 независимых review зафиксировано · quality verdict ещё отсутствует`, tone: "warning" };
return { text: "Проверочный набор готов · корректность object graph ещё не измерена", tone: "warning" };
}
function ReviewResult({ rigLabel, result }: { rigLabel: string; result: M48GateStatus }) {
const status = reviewStatus(result);
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="M4.8 · проверяем, видит ли система реальные объекты"
description="Worker 006 уже поставил рамки на связанных кадрах RIGHT-камеры. M4.8 показывает его ответ поверх синхронных CAMERA + LiDAR: оператор подтверждает правильные рамки и исправляет только false positive, miss и неточную геометрию."
status={status.text}
statusTone={status.tone}
facts={[
{ label: "Конфигурация", value: `${rigLabel} RIGHT · camera + prediction-free current spatial evidence` },
{ label: "Покрытие", value: `${result.clipCount} клипов · ${result.frameCount} кадров` },
{ label: "Авторазметка", value: `Worker 006 · ${result.seedObjectCount.toLocaleString("ru-RU")} frozen-рамок` },
{ label: "Correction", value: `${result.correctionReviewedClipCount}/${result.clipCount} клипов · ${result.correctionState}` },
{ label: "Authority", value: "REPLAY-SIMULATED · commands OFF · actuation OFF" },
]}
brief={{
question: "Не пропускает ли система реальный объект, не придумывает ли лишний, не теряет ли его между кадрами и правильно ли понимает геометрию, свежесть, движение и опасность в коридоре движения?",
approach: "RIGHT-камера, рамки Worker 006 и синхронный LiDAR идут на одном таймлайне. На каждом из 24 клипов оператор удаляет лишнее, добавляет пропущенное, двигает или ресайзит неточную рамку и подтверждает клип.",
principalResult: result.correctionState === "frozen"
? "Проверка зафиксирована: исходный seed и все ручные изменения связаны одной immutable дельтой."
: `Готовы ${result.seedObjectCount.toLocaleString("ru-RU")} автоматических рамок на ${result.frameCount} кадрах; ручная работа начинается с результата системы, а не с пустого кадра.`,
limitation: "Correction выполнен с видимым ответом Worker 006 и потому не является independent ground truth. Отдельный двухрецензентный truth seal всё ещё нужен для формального quality gate; physical live, навигация и команды моторам запрещены.",
}}
method={{
completeness: "complete",
executionClass: "hybrid",
pipelineId: "m48-class-free-object-quality/v1",
components: [
{ kind: "source", name: result.packId, version: "immutable connected clips", role: "camera/current-spatial evidence + frozen Worker 006 seed", identitySha256: result.packId.split("-").at(-1) ?? null },
{ kind: "algorithm", name: "frozen-candidate-seeded correction", version: "object-tracklet/v1", role: "editable assisted evidence without semantic classes", identitySha256: null },
{ kind: "algorithm", name: "independent dual review + adjudication", version: "release-gate/v1", role: "separate formal truth-seal workflow", identitySha256: result.truthSealId?.split("-").at(-1) ?? null },
],
}}
/>
)}
evidence={(
<LaboratoryEvidence eyebrow="M4.8 VISUAL EVIDENCE · CAMERA + 3D" title="Один объект в RIGHT-камере и LiDAR на общем времени" kind="recorded-replay" resizable>
<M48ReviewPackVisual packId={result.packId} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Что дал прогон: Worker 006 поставил рамки; оператор проверяет только ошибки"
status={status.text}
statusTone={status.tone}
metrics={[
{ label: "Worker boxes", value: result.seedObjectCount.toLocaleString("ru-RU"), hint: `${result.frameCount} кадров · immutable seed` },
{ label: "Clips checked", value: `${result.correctionReviewedClipCount}/${result.clipCount}`, hint: "operator correction progress" },
{ label: "Assisted evidence", value: result.correctionState === "frozen" ? "FROZEN" : "OPEN", hint: "candidate-visible · not independent truth" },
{ label: "Authority", value: "OFF", hint: "physical live · commands · actuation" },
]}
conclusion={{
proved: `Worker 006 воспроизводимо выдал ${result.seedObjectCount.toLocaleString("ru-RU")} рамок; CAMERA, 3D/PLAN и рамки связаны одним временем. После freeze будет сохранена точная дельта подтверждений, исправлений, false positive и miss.`,
notProved: "Пока оператор не проверил 24/24 клипа, корректность этих рамок не подтверждена. Даже завершённый assisted correction не заменяет независимую ground truth и не даёт motor/planner authority.",
decision: result.correctionState === "frozen" ? "Assisted regression evidence закрыто; для формального release gate отдельно выполнить два blind review и adjudication." : "Открыть проверку Worker 006, пройти все 24 клипа, исправить ошибки и зафиксировать evidence.",
}}
/>
)}
/>
);
}
function QualityResult({ rigLabel, result }: { rigLabel: string; result: M48QualityResult }) {
const gateCount = Object.keys(result.gates).length;
const passedGates = Object.values(result.gates).filter(Boolean).length;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="M4.8 · object-centric source quality gate"
description="Frozen M4.7 graph сопоставлен с adjudicated class-free truth. Acceptance считается только на sealed validation split; development остаётся диагностическим и не может улучшить gate."
status={result.accepted ? "Object-centric source quality принята" : "Quality gate не пройден · открыт bounded failure atlas"}
statusTone={result.accepted ? "success" : "warning"}
facts={[
{ label: "Конфигурация", value: `${rigLabel} RIGHT · frozen graph vs adjudicated truth` },
{ label: "Pack", value: result.packId },
{ label: "Truth seal", value: result.truthSealId },
{ label: "Acceptance", value: "VALIDATION ONLY · development informational" },
{ label: "Authority", value: "SOURCE-SCOPED · commands OFF · actuation OFF" },
]}
brief={{
question: "Проходит ли canonical graph минимальный object-centric quality gate на sealed validation split RAVNOVES00 source?",
approach: "Per-frame class-free matching выполняется только после двух независимых reviews и adjudication. Gate берёт metrics только из validation; каждый отказ связан с конкретными клипами и кадрами в failure atlas.",
principalResult: `${passedGates}/${gateCount} validation gates passed · presence P/R ${percent(result.metrics.obstaclePresencePrecision)} / ${percent(result.metrics.obstaclePresenceRecall)} · ${result.metrics.failureCaseCount} failure cases.`,
limitation: "Результат ограничен recorded source и не доказывает realtime live, физическую collision safety, planning или motor control.",
}}
method={{
completeness: "complete",
executionClass: "hybrid",
pipelineId: "m48-object-centric-quality/v1",
components: [
{ kind: "source", name: result.packId, version: "frozen-before-label-reveal", role: "candidate graph and connected source clips", identitySha256: result.packId.split("-").at(-1) ?? null },
{ kind: "source", name: result.truthSealId, version: "two reviewers + adjudication", role: "class-free object truth", identitySha256: result.truthSealId.split("-").at(-1) ?? null },
{ kind: "algorithm", name: "object-centric quality scorer", version: "v1", role: "per-frame matching, gates and failure atlas", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>
)}
evidence={(
<LaboratoryEvidence eyebrow="M4.8 VISUAL EVIDENCE · FAILURE ATLAS" title="Точные bounded cases: SOURCE / TRUTH / GRAPH / OVERLAY" kind="diagnostic-model" resizable>
<M48FailureAtlasVisual resultId={result.resultId} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title={result.accepted ? "Object-centric gate принят; можно готовить recorded realtime release candidate" : "Gate отклонён; исправления привязаны к bounded failure clusters"}
status={`${passedGates}/${gateCount} validation gates · ${result.metrics.failureCaseCount} failure cases · ${result.metrics.unknownPredictionCount} unknown predictions`}
statusTone={result.accepted ? "success" : "warning"}
metrics={[
{ label: "Presence P / R", value: `${percent(result.metrics.obstaclePresencePrecision)} / ${percent(result.metrics.obstaclePresenceRecall)}`, hint: "class-free object presence" },
{ label: "Critical recall", value: percent(result.metrics.criticalCorridorObstacleRecall), hint: "objects intersecting the virtual corridor" },
{ label: "Geometry / freshness", value: `${percent(result.metrics.geometryAssociationCorrectness)} / ${percent(result.metrics.freshnessCorrectness)}`, hint: "adjudicated state correctness" },
{ label: "Motion / false not-threat", value: `${percent(result.metrics.motionDecisionCorrectness)} / ${result.metrics.criticalNotThreatCount}`, hint: "critical corridor violations · conservative unknown retained" },
]}
conclusion={{
proved: `На sealed validation split граф прошёл ${passedGates}/${gateCount} object-centric gates; development metrics не участвовали в acceptance, каждый отказ и unknown имеет frame-level cause.`,
notProved: "Не доказаны physical live, измеренная collision safety, navigation planner, motor commands или переносимость на другие маршруты.",
decision: result.accepted ? "Открыть M4.9 recorded realtime release-candidate gate без расширения authority." : "Исправлять только кластеры из failure atlas, повторно заморозить candidate до label reveal и пересчитать M4.8.",
}}
/>
)}
/>
);
}
export function M48ObjectCentricQualityResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: M48AdvancedResult;
}) {
return result.kind === "review"
? <ReviewResult rigLabel={rigLabel} result={result} />
: <QualityResult rigLabel={rigLabel} result={result} />;
}
@@ -0,0 +1,84 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { M48SmallStaticRegressionResult } from "../../core/laboratory/m48SmallStaticRegression";
import { M48SmallStaticRegressionVisual } from "./M48SmallStaticRegressionVisual";
function percent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
}
export function M48SmallStaticPassageRegressionResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: M48SmallStaticRegressionResult;
}) {
const status = result.accepted
? "Development regression target пройден"
: `Worker 006 пропустил ${result.metrics.workerMissedAnchorCount}/${result.metrics.assistedAnchorCount} assisted-якорей`;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="M4.8R1 · мелкие статические ограничения проезда"
description="Отдельный append-only прогон внутри M4.8 проверяет, покрывает ли замороженный Worker 006 вручную добавленные столбики, полусферы, урны и другие малые статические ограничения. Текущий M4.8 correction не изменяется."
status={status}
statusTone={result.accepted ? "success" : "warning"}
facts={[
{ label: "Конфигурация", value: `${rigLabel} RIGHT · camera + prediction-free current spatial evidence` },
{ label: "Пайплайн", value: result.pipelineId },
{ label: "Эксперимент", value: result.experimentId },
{ label: "Прогон", value: `${result.runLabel} · immutable ${result.resultId}` },
{ label: "Authority", value: "REPLAY-SIMULATED · commands OFF · actuation OFF" },
]}
brief={{
question: "Находит ли текущий Worker 006 малые статические ограничения прохода, которые оператору пришлось добавить вручную при assisted correction?",
approach: `Зафиксирован отдельный снимок ${result.metrics.assistedAnchorCount} operator-added якорей на ${result.metrics.anchorClipCount} клипах. На точном исходном кадре каждый якорь сопоставлен с frozen-ответом Worker 006 по IoU ≥ ${result.metrics.extentIouThreshold.toFixed(2)}.`,
principalResult: `${result.metrics.workerRecalledAnchorCount}/${result.metrics.assistedAnchorCount} якорей покрыты Worker 006; ${result.metrics.workerMissedAnchorCount} не имеют совпадающей frozen-рамки.`,
limitation: "Это candidate-visible assisted seed, намеренно собранный из ручных добавлений, поэтому он полезен как regression baseline, но не является independent ground truth. Camera bbox не является 3D-коллайдером и не выдаёт planner/safety authority.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: result.pipelineId,
components: [
{ kind: "source", name: result.packId, version: "immutable Worker 006 pack", role: "frozen candidate output + exact camera/current-spatial evidence", identitySha256: result.packId.split("-").at(-1) ?? null },
{ kind: "source", name: "M4.8 assisted correction snapshot", version: "revision-bound", role: "operator-added anchors · not independent truth", identitySha256: null },
{ kind: "algorithm", name: "small-static assisted-anchor comparator", version: "v1", role: "exact-frame class-free IoU regression", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>
)}
evidence={(
<LaboratoryEvidence eyebrow="M4.8R1 VISUAL EVIDENCE · ASSISTED ANCHOR + WORKER" title="Точный кадр: ручной якорь и frozen-ответ Worker 006" kind="recorded-replay" resizable>
<M48SmallStaticRegressionVisual resultId={result.resultId} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Что дал прогон: зафиксирован измеримый baseline пропусков малых ограничений"
status={status}
statusTone={result.accepted ? "success" : "warning"}
metrics={[
{ label: "Assisted recall", value: percent(result.metrics.assistedAnchorRecall), hint: `target ${percent(result.metrics.minimumAssistedAnchorRecall)} · diagnostic only` },
{ label: "Recalled / missed", value: `${result.metrics.workerRecalledAnchorCount} / ${result.metrics.workerMissedAnchorCount}`, hint: `IoU ≥ ${result.metrics.extentIouThreshold.toFixed(2)}` },
{ label: "Объезд или запас", value: result.metrics.requiresAvoidanceOrClearanceCount.toLocaleString("ru-RU"), hint: "operator-marked small static constraints" },
{ label: "Independent truth", value: "НЕТ", hint: "assisted candidate-visible evidence" },
]}
conclusion={{
proved: `Worker 006 детерминированно покрывает ${result.metrics.workerRecalledAnchorCount}/${result.metrics.assistedAnchorCount} вручную добавленных якорей на точных кадрах; результат сохранён отдельно и не перезаписывает correction-сессию.`,
notProved: "Не измерены unbiased precision/recall, 3D clearance, проезжаемость конкретного шасси, realtime live или collision safety.",
decision: result.accepted
? "Сохранить прогон как development baseline и отдельно открыть independent truth evaluation."
: "Использовать пропуски как bounded regression set для следующей версии Worker 006; после обновления выполнить новый append-only run на том же снимке и только затем — independent truth gate.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,235 @@
import { useEffect, useMemo, useState } from "react";
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchM48ReviewSourceCatalog,
type M48ReviewTracklet,
} from "../../core/laboratory/m48ObjectCentricQuality";
import {
fetchM48SmallStaticRegressionCase,
fetchM48SmallStaticRegressionCases,
type M48SmallStaticRegressionCase,
type M48SmallStaticRegressionCaseSummary,
} from "../../core/laboratory/m48SmallStaticRegression";
import {
M48BlindClipPlayer,
type M48BlindEvidenceMode,
} from "./annotation/M48BlindClipPlayer";
import { M48EvidenceModeRail } from "./annotation/M48EvidenceModeControls";
import { useM48SpatialClipPlayback } from "./annotation/useM48SpatialClipPlayback";
function message(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "M4.8R1 evidence недоступно.";
}
function exactFrameTracklets(item: M48SmallStaticRegressionCase): readonly M48ReviewTracklet[] {
const sequence = item.anchor.sequence;
const state = (
objectId: string,
extentXyxy: readonly [number, number, number, number],
geometryAssociation: M48ReviewTracklet["stateSegments"][number]["geometryAssociation"],
freshness: M48ReviewTracklet["stateSegments"][number]["freshness"],
motion: M48ReviewTracklet["stateSegments"][number]["motion"],
threat: M48ReviewTracklet["stateSegments"][number]["threat"],
criticalCorridorObstacle: boolean,
): M48ReviewTracklet => ({
objectId,
firstSequence: sequence,
lastSequence: sequence,
keyframes: [{ sequence, extentXyxy, visibility: "visible" }],
stateSegments: [{
startSequence: sequence,
endSequence: sequence,
geometryAssociation,
freshness,
motion,
threat,
criticalCorridorObstacle,
}],
notes: null,
});
return [
state(
`ASSISTED · ${item.anchor.objectId}`,
item.anchor.extentXyxy,
item.anchor.geometryAssociation,
item.anchor.freshness,
item.anchor.motion,
item.anchor.threat,
item.anchor.requiresAvoidanceOrClearance,
),
...item.comparison.workerObjects.map((object) => state(
`WORKER · ${object.predictionId}`,
object.extentXyxy,
object.geometryAssociation,
object.freshness,
object.motion,
object.threat,
false,
)),
];
}
export function M48SmallStaticRegressionVisual({ resultId }: { resultId: string }) {
const [cases, setCases] = useState<readonly M48SmallStaticRegressionCaseSummary[]>([]);
const [caseIndex, setCaseIndex] = useState(0);
const [item, setItem] = useState<M48SmallStaticRegressionCase | null>(null);
const [catalog, setCatalog] = useState<Awaited<ReturnType<typeof fetchM48ReviewSourceCatalog>> | null>(null);
const [sequence, setSequence] = useState(1);
const [mode, setMode] = useState<M48BlindEvidenceMode>("camera");
const [cameraVisible, setCameraVisible] = useState(true);
const [expanded, setExpanded] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchM48SmallStaticRegressionCases(resultId, { signal: controller.signal })
.then((next) => {
if (!controller.signal.aborted) setCases(next);
})
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
.finally(() => !controller.signal.aborted && setLoading(false));
return () => controller.abort();
}, [resultId]);
useEffect(() => {
const selected = cases[caseIndex];
if (!selected) {
setItem(null);
return;
}
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchM48SmallStaticRegressionCase(resultId, selected.anchorId, { signal: controller.signal })
.then((next) => {
if (controller.signal.aborted) return;
setItem(next);
setSequence(next.anchor.sequence);
})
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
.finally(() => !controller.signal.aborted && setLoading(false));
return () => controller.abort();
}, [caseIndex, cases, resultId]);
useEffect(() => {
if (!item) return;
const controller = new AbortController();
setCatalog(null);
void fetchM48ReviewSourceCatalog(item.packId, { signal: controller.signal })
.then((next) => !controller.signal.aborted && setCatalog(next))
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)));
return () => controller.abort();
}, [item?.packId]);
const clip = item && catalog
? catalog.clips.find((candidate) => candidate.clipId === item.anchor.clipId) ?? null
: null;
const spatialEnabled = Boolean(
catalog?.evidenceCapabilities.currentPointCloudBodyXyzM
&& catalog.evidenceCapabilities.rig
&& catalog.evidenceCapabilities.virtualCorridor,
);
const spatial = useM48SpatialClipPlayback({
packId: item?.packId ?? "",
clip,
sequence,
enabled: mode !== "camera" && spatialEnabled,
});
const tracklets = useMemo(
() => item ? exactFrameTracklets(item) : [],
[item],
);
return (
<LaboratoryEvidenceViewer
label="M4.8R1 assisted-anchor regression"
className="m48-atlas-visual"
mode={mode}
modes={[]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
modeControlsVisible={false}
chromeLayout="stacked"
actions={(
<>
<IconButton
label="Предыдущий assisted-якорь"
disabled={!cases.length}
onClick={() => setCaseIndex((current) => (current - 1 + cases.length) % cases.length)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий assisted-якорь"
disabled={!cases.length}
onClick={() => setCaseIndex((current) => (current + 1) % cases.length)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</>
)}
overlay={item ? (
<div className="m48-atlas-visual__case">
<StatusBadge tone={item.comparison.matchedAtThreshold ? "success" : "warning"}>
{item.comparison.matchedAtThreshold ? "WORKER RECALL" : "WORKER MISS"}
</StatusBadge>
<strong>{caseIndex + 1}/{cases.length} · {item.anchor.clipId} · кадр {item.anchor.sequence}</strong>
<small>ASSISTED-якорь, не independent truth · best IoU {item.comparison.bestIou.toFixed(3)}</small>
</div>
) : null}
>
<div className="m48-evidence-stage">
{loading ? (
<div className="m48-atlas-visual__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
Загружаем M4.8R1 bounded case
</div>
) : error ? (
<div className="m48-atlas-visual__state" role="alert"><Icon name="alert" size={18} />{error}</div>
) : clip && catalog?.cameraPlayback && item ? (
<M48BlindClipPlayer
cameraPlayback={catalog.cameraPlayback}
clip={clip}
sequence={sequence}
mode={mode}
cameraVisible={cameraVisible}
tracklets={tracklets}
selectedObjectId={`ASSISTED · ${item.anchor.objectId}`}
editable={false}
drawing={false}
spatialFrame={spatial.frame}
spatialLoading={spatial.loading}
spatialError={spatial.error}
spatialEvidenceAvailable={spatialEnabled}
onSequenceChange={setSequence}
onDrawingChange={() => undefined}
onSelectedObjectIdChange={() => undefined}
onTrackletsChange={() => undefined}
/>
) : (
<div className="m48-atlas-visual__state" role="status">
<Icon name="alert" size={18} />Точный источник M4.8R1 недоступен.
</div>
)}
{catalog ? (
<M48EvidenceModeRail
mode={mode}
cameraVisible={cameraVisible}
spatialAvailable={spatialEnabled}
onModeChange={setMode}
onCameraVisibleChange={setCameraVisible}
/>
) : null}
</div>
</LaboratoryEvidenceViewer>
);
}
@@ -0,0 +1,291 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Button,
Checker,
Icon,
IconButton,
SegmentedControl,
Select,
StatusBadge,
TextField,
ToastStack,
Window,
WindowFooterActions,
type ToastItem,
} from "@nodedc/ui-react";
import {
createM48AdjudicationSession,
evaluateM48Adjudication,
fetchM48ReviewSourceCatalog,
freezeM48AdjudicationSession,
saveM48AdjudicationSession,
type M48AdjudicationSession,
type M48GateStatus,
type M48ReviewClipDraft,
type M48ReviewSourceCatalog,
type M48ReviewTracklet,
} from "../../../core/laboratory/m48ObjectCentricQuality";
import { LaboratoryReviewWorkspaceFrame } from "../../../components/laboratory/LaboratoryReviewWorkspaceFrame";
import { M48BlindClipPlayer, type M48BlindEvidenceMode } from "./M48BlindClipPlayer";
import { useM48SpatialClipPlayback } from "./useM48SpatialClipPlayback";
import { M48EvidenceModeRail } from "./M48EvidenceModeControls";
const REVIEW_LAYERS = [
{ value: "reviewer-a", label: "REVIEWER A" },
{ value: "reviewer-b", label: "REVIEWER B" },
{ value: "decision", label: "РЕШЕНИЕ" },
] as const;
type ReviewLayer = typeof REVIEW_LAYERS[number]["value"];
function message(error: unknown): string {
return error instanceof Error && error.message.trim() ? error.message : "M4.8 adjudication не выполнена.";
}
function operationKey(packId: string): string {
const storageKey = `missioncore:m48:${packId}:adjudication-operation-key`;
const current = localStorage.getItem(storageKey);
if (current) return current;
const created = `adjudication-${crypto.randomUUID()}`;
localStorage.setItem(storageKey, created);
return created;
}
function decisionCopy(clip: M48ReviewClipDraft): M48ReviewClipDraft {
return { ...clip, reviewState: "adjudicated", tracklets: clip.tracklets.map((tracklet) => ({ ...tracklet, keyframes: tracklet.keyframes.map((keyframe) => ({ ...keyframe })), stateSegments: tracklet.stateSegments.map((segment) => ({ ...segment })) })) };
}
export function M48AdjudicationWorkspace({
gate,
returnFocusTarget,
onClose,
onChanged,
}: {
gate: M48GateStatus;
returnFocusTarget?: HTMLElement | null;
onClose: () => void;
onChanged?: () => void;
}) {
const [catalog, setCatalog] = useState<M48ReviewSourceCatalog | null>(null);
const [session, setSession] = useState<M48AdjudicationSession | null>(null);
const [drafts, setDrafts] = useState<ReadonlyMap<string, M48ReviewClipDraft>>(new Map());
const [clipId, setClipId] = useState("");
const [sequence, setSequence] = useState(1);
const [reviewLayer, setReviewLayer] = useState<ReviewLayer>("reviewer-a");
const [evidenceMode, setEvidenceMode] = useState<M48BlindEvidenceMode>("camera");
const [cameraVisible, setCameraVisible] = useState(true);
const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null);
const [drawing, setDrawing] = useState(false);
const [dirty, setDirty] = useState(false);
const [busy, setBusy] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [freezeOpen, setFreezeOpen] = useState(false);
const [adjudicatorId, setAdjudicatorId] = useState("");
const [resolvedAttested, setResolvedAttested] = useState(false);
const [blindAttested, setBlindAttested] = useState(false);
const [toasts, setToasts] = useState<ToastItem[]>([]);
const notify = useCallback((toast: Omit<ToastItem, "id">) => setToasts((current) => [...current, { ...toast, id: crypto.randomUUID() }]), []);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
void fetchM48ReviewSourceCatalog(gate.packId, { signal: controller.signal })
.then((next) => {
if (controller.signal.aborted) return;
setCatalog(next);
const first = next.clips[0];
if (first) {
setClipId(first.clipId);
setSequence(first.startSequence);
}
})
.catch((caught: unknown) => !controller.signal.aborted && setError(message(caught)))
.finally(() => !controller.signal.aborted && setLoading(false));
return () => controller.abort();
}, [gate.packId]);
const clip = useMemo(() => catalog?.clips.find((item) => item.clipId === clipId) ?? null, [catalog, clipId]);
const reviewerA = session?.reviewInputs.find(({ reviewerSlot }) => reviewerSlot === 1)?.clips.find((item) => item.clipId === clipId) ?? null;
const reviewerB = session?.reviewInputs.find(({ reviewerSlot }) => reviewerSlot === 2)?.clips.find((item) => item.clipId === clipId) ?? null;
const decision = drafts.get(clipId) ?? null;
const visibleDraft = reviewLayer === "reviewer-a" ? reviewerA : reviewLayer === "reviewer-b" ? reviewerB : decision;
const selectedDecisionTracklet = decision?.tracklets.find(({ objectId }) => objectId === selectedObjectId) ?? null;
const editable = Boolean(session && reviewLayer === "decision" && !["adjudication-frozen", "evaluated"].includes(session.state));
const spatialEnabled = Boolean(catalog?.evidenceCapabilities.currentPointCloudBodyXyzM && catalog.evidenceCapabilities.rig && catalog.evidenceCapabilities.virtualCorridor);
const {
frame: spatial,
loading: spatialLoading,
error: spatialError,
} = useM48SpatialClipPlayback({
packId: gate.packId,
clip,
sequence,
enabled: evidenceMode !== "camera" && spatialEnabled,
});
const createSession = async () => {
setBusy(true);
setError(null);
try {
const next = await createM48AdjudicationSession(gate.packId, operationKey(gate.packId));
setSession(next);
setDrafts(new Map(next.clips.map((item) => [item.clipId, item])));
notify({ tone: "success", title: "Adjudication открыта", description: "Reviewer A/B видны без model predictions." });
} catch (caught) {
setError(message(caught));
} finally {
setBusy(false);
}
};
const setDecision = (next: M48ReviewClipDraft) => {
setDrafts((current) => new Map(current).set(next.clipId, next));
setDirty(true);
setReviewLayer("decision");
};
const updateDecisionTrackState = (patch: Partial<M48ReviewTracklet["stateSegments"][number]>) => {
if (!decision || !selectedDecisionTracklet) return;
setDecision({
...decision,
reviewState: "pending",
noObject: null,
tracklets: decision.tracklets.map((tracklet) => tracklet.objectId === selectedDecisionTracklet.objectId
? { ...tracklet, stateSegments: tracklet.stateSegments.map((segment) => ({ ...segment, ...patch })) }
: tracklet),
});
};
const save = async () => {
if (!session) return;
setBusy(true);
try {
const clips = session.clips.map((item) => drafts.get(item.clipId) ?? item);
const saved = await saveM48AdjudicationSession(session, session.title, clips, `save-${session.revision + 1}-${crypto.randomUUID()}`);
setSession(saved);
setDrafts(new Map(saved.clips.map((item) => [item.clipId, item])));
setDirty(false);
notify({ tone: "success", title: "Решения сохранены", description: `${saved.resolvedClipCount}/${saved.clipCount} клипов согласовано.` });
onChanged?.();
} catch (caught) {
setError(message(caught));
} finally {
setBusy(false);
}
};
const freeze = async () => {
if (!session || dirty || !session.complete || !adjudicatorId.trim() || !resolvedAttested || !blindAttested) return;
setBusy(true);
try {
const frozen = await freezeM48AdjudicationSession(session, adjudicatorId.trim());
setSession(frozen);
setFreezeOpen(false);
notify({ tone: "success", title: "Truth seal создан", description: "Frozen adjudication готова к одноразовой оценке." });
onChanged?.();
} catch (caught) {
setError(message(caught));
} finally {
setBusy(false);
}
};
const evaluate = async () => {
if (!session || session.state !== "adjudication-frozen") return;
setBusy(true);
try {
const evaluated = await evaluateM48Adjudication(session, `evaluate-${crypto.randomUUID()}`);
setSession(evaluated);
notify({ tone: "success", title: "M4.8 рассчитана", description: evaluated.qualityResultId ?? "Результат зарегистрирован." });
onChanged?.();
} catch (caught) {
setError(message(caught));
} finally {
setBusy(false);
}
};
const requestClose = () => {
if (!dirty || window.confirm("Закрыть без сохранения?")) onClose();
};
return (
<LaboratoryReviewWorkspaceFrame
ariaLabel="M4.8 adjudication"
onClose={requestClose}
interactionEnabled={!freezeOpen}
returnFocusTarget={returnFocusTarget}
toolbar={(
<>
<div className="m48-review-workspace__toolbar-group">
<Button size="compact" variant="secondary" icon={<Icon name="plus" size={16} />} disabled={busy || Boolean(session)} onClick={() => void createSession()}>Открыть adjudication</Button>
<Button size="compact" variant={drawing ? "accent" : "secondary"} icon={<Icon name="edit" size={16} />} disabled={!editable} onClick={() => setDrawing((value) => !value)}>Объект</Button>
<Button size="compact" variant="primary" icon={<Icon name="save" size={16} />} disabled={!editable || !dirty || busy} onClick={() => void save()}>Сохранить</Button>
<Button size="compact" variant="accent" icon={<Icon name="check" size={16} />} disabled={!session?.complete || dirty || busy || session.state !== "saved"} onClick={() => setFreezeOpen(true)}>Truth seal</Button>
<Button size="compact" variant="accent" disabled={session?.state !== "adjudication-frozen" || busy} onClick={() => void evaluate()}>Рассчитать gate</Button>
</div>
<div className="m48-review-workspace__toolbar-group">
<Select label="Клип" value={clipId} options={(catalog?.clips ?? []).map((item) => ({ value: item.clipId, label: `${item.ordinal}/${catalog?.clipCount ?? 0} · ${item.clipId} · ${drafts.get(item.clipId)?.reviewState ?? "pending"}` }))} disabled={!catalog} searchable menuWidth={380} onChange={(value) => {
const next = catalog?.clips.find((item) => item.clipId === value);
if (!next) return;
setClipId(value);
setSequence(next.startSequence);
setSelectedObjectId(null);
}} />
<StatusBadge tone={session?.state === "evaluated" ? "success" : dirty ? "warning" : session ? "neutral" : "warning"}>{session ? `${session.state} · ${session.resolvedClipCount}/${session.clipCount}` : "Adjudication не создана"}</StatusBadge>
<IconButton label="Закрыть M4.8 adjudication" onClick={requestClose}><Icon name="close" size={16} /></IconButton>
</div>
</>
)}
stage={(
<div className="m48-evidence-stage">
{loading ? <div className="m48-review-workspace__state" role="status"><span className="busy-indicator" aria-hidden="true" />Загружаем source-only клипы</div>
: !catalog || !catalog.cameraPlayback || !clip ? <div className="m48-review-workspace__state" role="alert"><Icon name="alert" size={18} />{error ?? "Источник недоступен."}</div>
: <M48BlindClipPlayer cameraPlayback={catalog.cameraPlayback} clip={clip} sequence={sequence} mode={evidenceMode} cameraVisible={cameraVisible} tracklets={visibleDraft?.tracklets ?? []} selectedObjectId={selectedObjectId} editable={editable} drawing={drawing} spatialFrame={spatial} spatialLoading={spatialLoading} spatialError={spatialError} spatialEvidenceAvailable={spatialEnabled} onSequenceChange={setSequence} onDrawingChange={setDrawing} onSelectedObjectIdChange={setSelectedObjectId} onTrackletsChange={(tracklets) => {
if (!decision) return;
setDecision({ ...decision, reviewState: "pending", noObject: null, tracklets });
}} />}
{catalog ? <M48EvidenceModeRail mode={evidenceMode} cameraVisible={cameraVisible} spatialAvailable={spatialEnabled} onModeChange={setEvidenceMode} onCameraVisibleChange={setCameraVisible} /> : null}
</div>
)}
inspector={(
<>
<div className="m48-review-workspace__source-state"><span>M4.8 · reviewer disagreement</span><strong>{clip ? `${clip.clipId} · frame ${sequence}` : "Источник проверяется"}</strong><small>Два независимых class-free review; frozen graph output остаётся скрыт.</small></div>
{(error || spatialError) && catalog ? <StatusBadge tone="danger">{error ?? spatialError}</StatusBadge> : null}
<SegmentedControl value={reviewLayer} items={[...REVIEW_LAYERS]} label="Reviewer inputs" onChange={setReviewLayer} />
{session && reviewerA && reviewerB && !["adjudication-frozen", "evaluated"].includes(session.state) ? (
<div className="m48-review-workspace__review-actions">
<Button size="compact" variant="secondary" onClick={() => setDecision(decisionCopy(reviewerA))}>Принять A</Button>
<Button size="compact" variant="secondary" onClick={() => setDecision(decisionCopy(reviewerB))}>Принять B</Button>
{decision ? <Checker checked={decision.reviewState === "adjudicated"} label={decision.noObject ? "Согласовано: объектов нет" : `Согласовано: ${decision.tracklets.length} tracklet`} onChange={(checked) => setDecision({ ...decision, reviewState: checked ? "adjudicated" : "pending", noObject: checked ? decision.tracklets.length === 0 : null })} /> : null}
</div>
) : null}
{editable && selectedDecisionTracklet ? (
<div className="m48-review-workspace__object-tools">
<strong>{selectedDecisionTracklet.objectId}</strong>
<Select disabled={!catalog?.evidenceCapabilities.geometryAssociation} label="Геометрия" value={selectedDecisionTracklet.stateSegments[0]?.geometryAssociation ?? "unknown"} options={[{ value: "associated", label: "Связана" }, { value: "unavailable", label: "Недоступна" }, { value: "ineligible", label: "Неприменима" }, { value: "unknown", label: "Неизвестно" }]} onChange={(value) => updateDecisionTrackState({ geometryAssociation: value as M48ReviewTracklet["stateSegments"][number]["geometryAssociation"] })} />
<Select disabled={!catalog?.evidenceCapabilities.freshness} label="Актуальность" value={selectedDecisionTracklet.stateSegments[0]?.freshness ?? "unavailable"} options={[{ value: "current", label: "Актуальна" }, { value: "held", label: "Удержана" }, { value: "stale", label: "Устарела" }, { value: "unavailable", label: "Недоступна" }]} onChange={(value) => updateDecisionTrackState({ freshness: value as M48ReviewTracklet["stateSegments"][number]["freshness"] })} />
<Select disabled={!catalog?.evidenceCapabilities.motion} label="Движение" value={selectedDecisionTracklet.stateSegments[0]?.motion ?? "unknown"} options={[{ value: "moving", label: "Движется" }, { value: "static", label: "Стоит" }, { value: "unknown", label: "Неизвестно" }, { value: "unsupported", label: "Не поддержано" }]} onChange={(value) => updateDecisionTrackState({ motion: value as M48ReviewTracklet["stateSegments"][number]["motion"] })} />
<Select disabled={!catalog?.evidenceCapabilities.threat} label="Угроза" value={selectedDecisionTracklet.stateSegments[0]?.threat ?? "unknown"} options={[{ value: "threat", label: "Угроза" }, { value: "not-threat", label: "Не угроза" }, { value: "unknown", label: "Неизвестно" }]} onChange={(value) => updateDecisionTrackState({ threat: value as M48ReviewTracklet["stateSegments"][number]["threat"] })} />
<Checker disabled={!catalog?.evidenceCapabilities.criticalCorridorObstacle} checked={selectedDecisionTracklet.stateSegments[0]?.criticalCorridorObstacle ?? false} label="Критический объект" onChange={(criticalCorridorObstacle) => updateDecisionTrackState({ criticalCorridorObstacle })} />
</div>
) : null}
</>
)}
overlays={(
<>
<Window open={freezeOpen} title="Создать M4.8 truth seal" subtitle="После freeze Reviewer A/B и adjudication станут immutable входом quality gate." size="md" closeOnBackdrop={!busy} closeOnEscape={!busy} onClose={() => !busy && setFreezeOpen(false)} footer={<WindowFooterActions><Button disabled={busy} onClick={() => setFreezeOpen(false)}>Отмена</Button><Button variant="accent" disabled={busy || !adjudicatorId.trim() || !resolvedAttested || !blindAttested} onClick={() => void freeze()}>Freeze adjudication</Button></WindowFooterActions>}>
<div className="m48-review-workspace__freeze-form">
<TextField label="Opaque adjudicator ID" value={adjudicatorId} maxLength={96} placeholder="adjudicator-1" onChange={(event) => setAdjudicatorId(event.target.value)} />
<Checker checked={resolvedAttested} label="Все 20–30 клипов согласованы" onChange={setResolvedAttested} />
<Checker checked={blindAttested} label="Model predictions до truth seal не просматривались" onChange={setBlindAttested} />
</div>
</Window>
<ToastStack items={toasts} onDismiss={(id) => setToasts((current) => current.filter((item) => item.id !== id))} />
</>
)}
/>
);
}
@@ -0,0 +1,528 @@
import {
useEffect,
useMemo,
useRef,
useState,
type PointerEvent as ReactPointerEvent,
type RefObject,
} from "react";
import { ActivityIndicator, Icon } from "@nodedc/ui-react";
import { LaboratoryRecordedClipPlayer } from "../../../components/laboratory/LaboratoryRecordedClipPlayer";
import { LaboratoryMetricEvidenceScene } from "../../../components/laboratory/LaboratoryMetricEvidenceScene";
import type {
M48RecordedCameraPlayback,
M48ReviewClipSource,
M48ReviewSpatialFrame,
M48ReviewTracklet,
} from "../../../core/laboratory/m48ObjectCentricQuality";
import { m48RecordedCameraSourceDescriptor } from "../../../core/laboratory/m48ObjectCentricQuality";
import type { M48BlindEvidenceMode } from "./M48EvidenceModeControls";
export type { M48BlindEvidenceMode } from "./M48EvidenceModeControls";
interface Rect {
x: number;
y: number;
width: number;
height: number;
}
type ResizeHandle = "nw" | "ne" | "sw" | "se";
interface BoxInteraction {
kind: "move" | "resize";
pointerId: number;
objectId: string;
start: readonly [number, number];
startClient: readonly [number, number];
current: readonly [number, number];
originalExtent: readonly [number, number, number, number];
handle?: ResizeHandle;
}
function interpolate(left: number, right: number, progress: number): number {
return left + (right - left) * progress;
}
export function interpolateM48Extent(
tracklet: M48ReviewTracklet,
sequence: number,
): readonly [number, number, number, number] | null {
if (sequence < tracklet.firstSequence || sequence > tracklet.lastSequence) return null;
const rightIndex = tracklet.keyframes.findIndex((keyframe) => keyframe.sequence >= sequence);
const right = tracklet.keyframes[rightIndex < 0 ? tracklet.keyframes.length - 1 : rightIndex];
if (!right) return null;
const left = tracklet.keyframes[Math.max(0, (rightIndex < 0 ? tracklet.keyframes.length : rightIndex) - 1)] ?? right;
if (left.sequence === right.sequence) return right.extentXyxy;
const progress = (sequence - left.sequence) / (right.sequence - left.sequence);
return right.extentXyxy.map((value, index) => interpolate(left.extentXyxy[index]!, value, progress)) as unknown as readonly [number, number, number, number];
}
export function createM48Tracklet(
objectId: string,
clip: M48ReviewClipSource,
extentXyxy: readonly [number, number, number, number],
spatialEvidenceAvailable = true,
sequence = clip.startSequence,
): M48ReviewTracklet {
return {
objectId,
firstSequence: sequence,
lastSequence: sequence,
keyframes: [{ sequence, extentXyxy, visibility: "visible" as const }],
stateSegments: [{
startSequence: sequence,
endSequence: sequence,
geometryAssociation: spatialEvidenceAvailable ? "unknown" : "unavailable",
freshness: "unavailable",
motion: spatialEvidenceAvailable ? "unknown" : "unsupported",
threat: "unknown",
criticalCorridorObstacle: false,
}],
notes: null,
};
}
export function nextM48ObjectId(tracklets: readonly M48ReviewTracklet[]): string {
const occupied = new Set(tracklets.map(({ objectId }) => objectId));
let ordinal = 1;
while (occupied.has(`object-${String(ordinal).padStart(2, "0")}`)) ordinal += 1;
return `object-${String(ordinal).padStart(2, "0")}`;
}
export function upsertM48Extent(
tracklet: M48ReviewTracklet,
sequence: number,
extentXyxy: readonly [number, number, number, number],
): M48ReviewTracklet {
const visibility = tracklet.keyframes.find((keyframe) => keyframe.sequence === sequence)?.visibility
?? tracklet.keyframes.filter((keyframe) => keyframe.sequence <= sequence).at(-1)?.visibility
?? "visible";
return {
...tracklet,
keyframes: [
...tracklet.keyframes.filter((keyframe) => keyframe.sequence !== sequence),
{ sequence, extentXyxy, visibility },
].sort((left, right) => left.sequence - right.sequence),
};
}
function useHostSize(ref: RefObject<HTMLDivElement | null>): Rect {
const [rect, setRect] = useState<Rect>({ x: 0, y: 0, width: 1, height: 1 });
useEffect(() => {
const host = ref.current;
if (!host) return;
const update = () => setRect({ x: 0, y: 0, width: Math.max(host.clientWidth, 1), height: Math.max(host.clientHeight, 1) });
update();
const observer = new ResizeObserver(update);
observer.observe(host);
return () => observer.disconnect();
}, [ref]);
return rect;
}
function imagePlane(host: Rect, naturalWidth: number, naturalHeight: number): Rect {
const scale = Math.min(host.width / Math.max(naturalWidth, 1), host.height / Math.max(naturalHeight, 1));
const width = naturalWidth * scale;
const height = naturalHeight * scale;
return { x: (host.width - width) / 2, y: (host.height - height) / 2, width, height };
}
function normalizedPoint(event: ReactPointerEvent<SVGSVGElement>, plane: Rect): readonly [number, number] | null {
const bounds = event.currentTarget.getBoundingClientRect();
const x = (event.clientX - bounds.left - plane.x) / Math.max(plane.width, 1);
const y = (event.clientY - bounds.top - plane.y) / Math.max(plane.height, 1);
if (x < 0 || x > 1 || y < 0 || y > 1) return null;
return [x, y];
}
function boundedNormalizedPoint(
clientX: number,
clientY: number,
svg: SVGSVGElement,
plane: Rect,
): readonly [number, number] {
const bounds = svg.getBoundingClientRect();
return [
Math.max(0, Math.min(1, (clientX - bounds.left - plane.x) / Math.max(plane.width, 1))),
Math.max(0, Math.min(1, (clientY - bounds.top - plane.y) / Math.max(plane.height, 1))),
];
}
function movedExtent(
original: readonly [number, number, number, number],
start: readonly [number, number],
current: readonly [number, number],
): readonly [number, number, number, number] {
const width = original[2] - original[0];
const height = original[3] - original[1];
const left = Math.max(0, Math.min(1 - width, original[0] + current[0] - start[0]));
const top = Math.max(0, Math.min(1 - height, original[1] + current[1] - start[1]));
return [left, top, left + width, top + height];
}
function resizedExtent(
original: readonly [number, number, number, number],
handle: ResizeHandle,
current: readonly [number, number],
): readonly [number, number, number, number] {
const minimum = 0.005;
let [left, top, right, bottom] = original;
if (handle.includes("n")) top = Math.min(current[1], bottom - minimum);
if (handle.includes("s")) bottom = Math.max(current[1], top + minimum);
if (handle.includes("w")) left = Math.min(current[0], right - minimum);
if (handle.includes("e")) right = Math.max(current[0], left + minimum);
return [left, top, right, bottom];
}
function interactionExtent(interaction: BoxInteraction) {
return interaction.kind === "move"
? movedExtent(interaction.originalExtent, interaction.start, interaction.current)
: resizedExtent(
interaction.originalExtent,
interaction.handle ?? "se",
interaction.current,
);
}
function extentsDiffer(
left: readonly [number, number, number, number],
right: readonly [number, number, number, number],
): boolean {
return left.some((value, index) => Math.abs(value - right[index]!) > 1e-6);
}
function interactionMoved(
start: readonly [number, number],
current: readonly [number, number],
): boolean {
return Math.hypot(current[0] - start[0], current[1] - start[1]) >= 3;
}
export function M48BlindClipPlayer({
cameraPlayback,
clip,
sequence,
mode,
cameraVisible,
tracklets,
selectedObjectId,
editable,
drawing,
spatialFrame,
spatialLoading,
spatialError,
spatialEvidenceAvailable = true,
onSequenceChange,
onDrawingChange,
onSelectedObjectIdChange,
onTrackletsChange,
}: {
cameraPlayback: M48RecordedCameraPlayback;
clip: M48ReviewClipSource;
sequence: number;
mode: M48BlindEvidenceMode;
cameraVisible: boolean;
tracklets: readonly M48ReviewTracklet[];
selectedObjectId: string | null;
editable: boolean;
drawing: boolean;
spatialFrame: M48ReviewSpatialFrame | null;
spatialLoading: boolean;
spatialError?: string | null;
spatialEvidenceAvailable?: boolean;
onSequenceChange: (sequence: number) => void;
onDrawingChange: (drawing: boolean) => void;
onSelectedObjectIdChange: (objectId: string | null) => void;
onTrackletsChange: (tracklets: readonly M48ReviewTracklet[]) => void;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const host = useHostSize(hostRef);
const [playing, setPlaying] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [drawStart, setDrawStart] = useState<readonly [number, number] | null>(null);
const [drawCurrent, setDrawCurrent] = useState<readonly [number, number] | null>(null);
const [boxInteraction, setBoxInteraction] = useState<BoxInteraction | null>(null);
const cameraSource = useMemo(
() => m48RecordedCameraSourceDescriptor(cameraPlayback),
[cameraPlayback],
);
const spatialReady = Boolean(
spatialFrame?.sequence === sequence
&& spatialFrame.sourceAvailable
&& spatialFrame.bodyFrameAvailable
&& spatialFrame.pointCloudBodyXyzM.length > 0,
);
const spatialVisible = mode !== "camera" && spatialEvidenceAvailable;
const effectiveCameraVisible = cameraVisible || !spatialVisible;
const plane = imagePlane(host, 1440, 1080);
useEffect(() => setPlaying(false), [clip.clipId]);
useEffect(() => {
setDrawStart(null);
setDrawCurrent(null);
setBoxInteraction(null);
}, [clip.clipId, sequence]);
const boxes = useMemo(() => tracklets.flatMap((tracklet) => {
const extent = interpolateM48Extent(tracklet, sequence);
return extent ? [{ tracklet, extent }] : [];
}), [sequence, tracklets]);
const finishDrawing = (event: ReactPointerEvent<SVGSVGElement>) => {
if (!editable || !drawing || !drawStart) return;
const end = normalizedPoint(event, plane) ?? drawCurrent;
setDrawStart(null);
setDrawCurrent(null);
if (!end) return;
const extent = [
Math.min(drawStart[0], end[0]),
Math.min(drawStart[1], end[1]),
Math.max(drawStart[0], end[0]),
Math.max(drawStart[1], end[1]),
] as const;
if (extent[2] - extent[0] < 0.01 || extent[3] - extent[1] < 0.01) return;
const selected = selectedObjectId
? tracklets.find((tracklet) => (
tracklet.objectId === selectedObjectId
&& sequence >= tracklet.firstSequence
&& sequence <= tracklet.lastSequence
))
: null;
if (selected) {
onTrackletsChange(tracklets.map((tracklet) => (
tracklet.objectId === selected.objectId
? upsertM48Extent(tracklet, sequence, extent)
: tracklet
)));
onDrawingChange(false);
return;
}
const objectId = nextM48ObjectId(tracklets);
onTrackletsChange([...tracklets, createM48Tracklet(objectId, clip, extent, spatialEvidenceAvailable, sequence)]);
onSelectedObjectIdChange(objectId);
onDrawingChange(false);
};
const finishBoxInteraction = (event: ReactPointerEvent<SVGSVGElement>) => {
if (!boxInteraction || boxInteraction.pointerId !== event.pointerId) return;
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
const current = boundedNormalizedPoint(
event.clientX,
event.clientY,
event.currentTarget,
plane,
);
const extent = interactionExtent({ ...boxInteraction, current });
if (
interactionMoved(
boxInteraction.startClient,
[event.clientX, event.clientY],
)
&& extentsDiffer(boxInteraction.originalExtent, extent)
) {
onTrackletsChange(tracklets.map((tracklet) => (
tracklet.objectId === boxInteraction.objectId
? upsertM48Extent(tracklet, sequence, extent)
: tracklet
)));
}
setBoxInteraction(null);
};
return (
<LaboratoryRecordedClipPlayer
source={cameraSource}
segmentCount={cameraPlayback.segmentCount}
frames={clip.frames}
sequence={sequence}
playing={playing}
playbackRate={playbackRate}
cameraPresentation={spatialVisible
? effectiveCameraVisible ? "companion" : "hidden"
: "primary"}
continuousPlayback
sourceCount={Number(effectiveCameraVisible) + Number(spatialVisible)}
cameraRef={hostRef}
onSequenceChange={onSequenceChange}
onPlayingChange={setPlaying}
onPlaybackRateChange={setPlaybackRate}
cameraOverlay={(<>
<div className="m48-clip-player__pane-label" data-pane="camera">
ПРАВАЯ КАМЕРА · СИНХРОННО · КАДР {sequence}
</div>
<svg
className="m48-clip-player__overlay"
viewBox={`0 0 ${host.width} ${host.height}`}
aria-label="Объектные tracklet-рамки без классов"
data-drawing={editable && drawing ? "true" : undefined}
onPointerDown={(event) => {
if (!drawing) {
onSelectedObjectIdChange(null);
return;
}
if (!editable) return;
setPlaying(false);
const point = normalizedPoint(event, plane);
if (point) {
event.currentTarget.setPointerCapture(event.pointerId);
setDrawStart(point);
setDrawCurrent(point);
}
}}
onPointerMove={(event) => {
if (drawStart) setDrawCurrent(normalizedPoint(event, plane));
if (boxInteraction?.pointerId === event.pointerId) {
setBoxInteraction({
...boxInteraction,
current: boundedNormalizedPoint(
event.clientX,
event.clientY,
event.currentTarget,
plane,
),
});
}
}}
onPointerUp={(event) => {
if (boxInteraction) finishBoxInteraction(event);
else finishDrawing(event);
}}
onPointerCancel={() => {
setDrawStart(null);
setDrawCurrent(null);
setBoxInteraction(null);
}}
>
{boxes.map(({ tracklet, extent }) => {
const displayedExtent = boxInteraction?.objectId === tracklet.objectId
? interactionExtent(boxInteraction)
: extent;
const [left, top, right, bottom] = displayedExtent;
return (
<g
key={tracklet.objectId}
data-selected={tracklet.objectId === selectedObjectId ? "true" : undefined}
onPointerDown={(event) => {
if (drawing) return;
event.stopPropagation();
onSelectedObjectIdChange(tracklet.objectId);
if (!editable || event.button !== 0) return;
setPlaying(false);
const svg = event.currentTarget.ownerSVGElement;
if (!svg) return;
svg.setPointerCapture(event.pointerId);
const point = boundedNormalizedPoint(event.clientX, event.clientY, svg, plane);
setBoxInteraction({
kind: "move",
pointerId: event.pointerId,
objectId: tracklet.objectId,
start: point,
startClient: [event.clientX, event.clientY],
current: point,
originalExtent: extent,
});
}}
>
<rect x={plane.x + left * plane.width} y={plane.y + top * plane.height} width={(right - left) * plane.width} height={(bottom - top) * plane.height} />
<text x={plane.x + left * plane.width} y={Math.max(14, plane.y + top * plane.height - 6)}>{tracklet.objectId}</text>
</g>
);
})}
{drawStart && drawCurrent ? (
<rect
className="m48-clip-player__draft-box"
x={plane.x + Math.min(drawStart[0], drawCurrent[0]) * plane.width}
y={plane.y + Math.min(drawStart[1], drawCurrent[1]) * plane.height}
width={Math.abs(drawCurrent[0] - drawStart[0]) * plane.width}
height={Math.abs(drawCurrent[1] - drawStart[1]) * plane.height}
/>
) : null}
{editable && !drawing && selectedObjectId ? boxes
.filter(({ tracklet }) => tracklet.objectId === selectedObjectId)
.flatMap(({ tracklet, extent }) => {
const displayedExtent = boxInteraction?.objectId === tracklet.objectId
? interactionExtent(boxInteraction)
: extent;
const [left, top, right, bottom] = displayedExtent;
return ([
["nw", left, top],
["ne", right, top],
["sw", left, bottom],
["se", right, bottom],
] as const).map(([handle, x, y]) => (
<circle
className="m48-clip-player__resize-handle"
data-handle={handle}
key={`${tracklet.objectId}-${handle}`}
cx={plane.x + x * plane.width}
cy={plane.y + y * plane.height}
r={6}
onPointerDown={(event) => {
if (event.button !== 0) return;
event.stopPropagation();
setPlaying(false);
const svg = event.currentTarget.ownerSVGElement;
if (!svg) return;
svg.setPointerCapture(event.pointerId);
const point = boundedNormalizedPoint(event.clientX, event.clientY, svg, plane);
setBoxInteraction({
kind: "resize",
pointerId: event.pointerId,
objectId: tracklet.objectId,
start: point,
startClient: [event.clientX, event.clientY],
current: point,
originalExtent: extent,
handle,
});
}}
/>
));
}) : null}
</svg>
</>)}
alternativeScene={(
<div
className="m48-clip-player__spatial-pane"
data-spatial-sequence={spatialReady ? sequence : undefined}
>
<div className="m48-clip-player__pane-label" data-pane="spatial">
{mode === "3d" ? "3D LIDAR" : "ПЛАН LIDAR"} · КАДР {sequence}
</div>
{spatialReady && spatialFrame ? (
<LaboratoryMetricEvidenceScene
pointCloudBodyXyzM={spatialFrame.pointCloudBodyXyzM}
localSurfaceBodyXyzM={[]}
obstacles={[]}
rig={spatialFrame.rig}
corridor={spatialFrame.corridor}
occupiedVoxelSizeM={spatialFrame.occupiedVoxelSizeM}
mode={mode === "3d" ? "3d" : "plan"}
label={`M4.8 пространственные данные источника · кадр ${sequence}`}
showCurrentIncrement
showLocalSurface={false}
showRollingMap={false}
/>
) : (
<div className="m48-clip-player__state" role={spatialLoading ? "status" : "alert"}>
{spatialLoading ? <ActivityIndicator size="compact" /> : <Icon name="alert" size={18} />}
{spatialLoading
? "Подготавливаем синхронный LiDAR-кадр"
: spatialError
? spatialError
: spatialFrame && !spatialFrame.sourceAvailable
? "Текущий LiDAR-кадр недоступен"
: spatialFrame && !spatialFrame.bodyFrameAvailable
? "LiDAR в системе координат корпуса для этого кадра недоступен"
: "Исходные пространственные данные для этого кадра недоступны"}
</div>
)}
</div>
)}
/>
);
}
@@ -0,0 +1,568 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Button,
Checker,
FieldFrame,
GlassSurface,
Icon,
IconButton,
Select,
StatusBadge,
TextField,
ToastStack,
Window,
WindowFooterActions,
type ToastItem,
} from "@nodedc/ui-react";
import {
createM48CorrectionSession,
fetchM48ReviewSourceCatalog,
freezeM48CorrectionSession,
saveM48CorrectionSession,
type M48CorrectionSession,
type M48GateStatus,
type M48ReviewClipDraft,
type M48ReviewSourceCatalog,
type M48ReviewTracklet,
} from "../../../core/laboratory/m48ObjectCentricQuality";
import { LaboratoryReviewWorkspaceFrame } from "../../../components/laboratory/LaboratoryReviewWorkspaceFrame";
import {
M48BlindClipPlayer,
interpolateM48Extent,
upsertM48Extent,
type M48BlindEvidenceMode,
} from "./M48BlindClipPlayer";
import { useM48SpatialClipPlayback } from "./useM48SpatialClipPlayback";
import { M48EvidenceModeRail } from "./M48EvidenceModeControls";
function errorMessage(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "Операция M4.8 не выполнена.";
}
function operationKey(packId: string): string {
const key = `missioncore:m48:${packId}:correction-operation-key`;
const stored = localStorage.getItem(key);
if (stored) return stored;
const created = `correction-${crypto.randomUUID()}`;
localStorage.setItem(key, created);
return created;
}
function draftMap(session: M48CorrectionSession): Map<string, M48ReviewClipDraft> {
return new Map(session.clips.map((clip) => [clip.clipId, clip]));
}
function stateLabel(session: M48CorrectionSession | null): string {
if (!session) return "Проверка загружается";
if (session.state === "frozen") return "Проверка завершена";
return `Проверено клипов: ${session.reviewedClipCount} из ${session.clipCount}`;
}
function clipOptionLabel(
ordinal: number,
clipCount: number,
clipId: string,
reviewState: M48ReviewClipDraft["reviewState"] | undefined,
): string {
const progress = `${String(ordinal).padStart(2, "0")}/${String(clipCount).padStart(2, "0")}`;
return `${progress} · ${clipId} · ${reviewState === "reviewed" ? "проверен" : "не проверен"}`;
}
type M48CorrectionSaveReason = "clip-status" | "object-edit";
interface M48CorrectionSaveRollback {
drafts: ReadonlyMap<string, M48ReviewClipDraft>;
dirty: boolean;
}
export function M48CorrectionWorkspace({
gate,
returnFocusTarget,
onClose,
onChanged,
}: {
gate: M48GateStatus;
returnFocusTarget?: HTMLElement | null;
onClose: () => void;
onChanged?: () => void;
}) {
const [catalog, setCatalog] = useState<M48ReviewSourceCatalog | null>(null);
const [session, setSession] = useState<M48CorrectionSession | null>(null);
const [drafts, setDrafts] = useState<ReadonlyMap<string, M48ReviewClipDraft>>(new Map());
const [selectedClipId, setSelectedClipId] = useState("");
const [sequence, setSequence] = useState(1);
const [mode, setMode] = useState<M48BlindEvidenceMode>("camera");
const [cameraVisible, setCameraVisible] = useState(true);
const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null);
const [drawing, setDrawing] = useState(false);
const [dirty, setDirty] = useState(false);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [freezeOpen, setFreezeOpen] = useState(false);
const [reviewerId, setReviewerId] = useState("");
const [candidateVisible, setCandidateVisible] = useState(false);
const [classFree, setClassFree] = useState(false);
const [toasts, setToasts] = useState<ToastItem[]>([]);
const savingRef = useRef(false);
const notify = useCallback((toast: Omit<ToastItem, "id">) => {
setToasts((current) => [...current, { ...toast, id: crypto.randomUUID() }]);
}, []);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
void Promise.all([
fetchM48ReviewSourceCatalog(gate.packId, { signal: controller.signal }),
createM48CorrectionSession(gate.packId, operationKey(gate.packId), { signal: controller.signal }),
])
.then(([next, correction]) => {
if (controller.signal.aborted) return;
setCatalog(next);
setSession(correction);
setDrafts(draftMap(correction));
const first = next.clips[0];
if (first) {
setSelectedClipId(first.clipId);
setSequence(first.startSequence);
}
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) setError(errorMessage(caught));
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [gate.packId]);
const clip = useMemo(
() => catalog?.clips.find((item) => item.clipId === selectedClipId) ?? null,
[catalog, selectedClipId],
);
const selectedClipIndex = useMemo(
() => catalog?.clips.findIndex((item) => item.clipId === selectedClipId) ?? -1,
[catalog, selectedClipId],
);
const currentDraft = clip ? drafts.get(clip.clipId) ?? null : null;
const selectedTracklet = currentDraft?.tracklets.find(({ objectId }) => objectId === selectedObjectId) ?? null;
useEffect(() => {
if (
selectedTracklet
&& (sequence < selectedTracklet.firstSequence || sequence > selectedTracklet.lastSequence)
) {
setSelectedObjectId(null);
}
}, [selectedTracklet, sequence]);
const spatialEnabled = Boolean(
catalog?.evidenceCapabilities.currentPointCloudBodyXyzM
&& catalog.evidenceCapabilities.rig
&& catalog.evidenceCapabilities.virtualCorridor,
);
const extentEnabled = Boolean(catalog?.evidenceCapabilities.obstaclePresenceAndExtent);
const editable = Boolean(session && session.state !== "frozen");
const editingEnabled = editable && !busy;
const {
frame: spatial,
loading: spatialLoading,
error: spatialError,
} = useM48SpatialClipPlayback({
packId: gate.packId,
clip,
sequence,
enabled: mode !== "camera" && spatialEnabled,
});
const setCurrentDraft = useCallback((next: M48ReviewClipDraft) => {
setDrafts((current) => {
const updated = new Map(current);
updated.set(next.clipId, next);
return updated;
});
setDirty(true);
}, []);
const save = async (
nextDrafts: ReadonlyMap<string, M48ReviewClipDraft> = drafts,
reason: M48CorrectionSaveReason = "object-edit",
rollback?: M48CorrectionSaveRollback,
) => {
if (!session || nextDrafts.size !== session.clipCount || savingRef.current) return false;
savingRef.current = true;
setBusy(true);
setError(null);
try {
const clips = session.clips.map((item) => nextDrafts.get(item.clipId) ?? item);
const saved = await saveM48CorrectionSession(
session,
session.title,
clips,
`save-${session.revision + 1}-${crypto.randomUUID()}`,
);
setSession(saved);
setDrafts(draftMap(saved));
setDirty(false);
notify({
tone: "success",
title: reason === "clip-status" ? "Статус клипа сохранён" : "Изменения объекта сохранены",
description: `${saved.reviewedClipCount}/${saved.clipCount} клипов проверено.`,
});
onChanged?.();
return true;
} catch (caught) {
const message = errorMessage(caught);
setError(message);
if (rollback) {
setDrafts(rollback.drafts);
setDirty(rollback.dirty);
} else {
setDirty(true);
}
notify({
tone: "error",
title: reason === "clip-status" ? "Статус клипа не сохранён" : "Изменения объекта не сохранены",
description: message,
});
return false;
} finally {
savingRef.current = false;
setBusy(false);
}
};
const freeze = async () => {
if (!session || !reviewerId.trim() || dirty || !session.complete || !candidateVisible || !classFree) return;
setBusy(true);
setError(null);
try {
const frozen = await freezeM48CorrectionSession(session, reviewerId.trim());
setSession(frozen);
setDrafts(draftMap(frozen));
setFreezeOpen(false);
setDrawing(false);
notify({ tone: "success", title: "Проверка Worker 006 зафиксирована", description: "Дельта correction сохранена как assisted evidence, не independent truth." });
onChanged?.();
} catch (caught) {
setError(errorMessage(caught));
} finally {
setBusy(false);
}
};
const updateSelectedTracklet = (patch: Partial<M48ReviewTracklet>) => {
if (!currentDraft || !selectedTracklet) return;
setCurrentDraft({
...currentDraft,
reviewState: "pending",
noObject: null,
tracklets: currentDraft.tracklets.map((tracklet) => (
tracklet.objectId === selectedTracklet.objectId ? { ...tracklet, ...patch } : tracklet
)),
});
};
const updateTrackState = (patch: Partial<M48ReviewTracklet["stateSegments"][number]>) => {
if (!selectedTracklet) return;
updateSelectedTracklet({
stateSegments: selectedTracklet.stateSegments.map((segment) => ({ ...segment, ...patch })),
});
};
const updateVisibility = (visibility: M48ReviewTracklet["keyframes"][number]["visibility"]) => {
if (!selectedTracklet) return;
const extent = interpolateM48Extent(selectedTracklet, sequence);
if (!extent) return;
const withKeyframe = upsertM48Extent(selectedTracklet, sequence, extent);
updateSelectedTracklet({
keyframes: withKeyframe.keyframes.map((keyframe) => (
keyframe.sequence === sequence ? { ...keyframe, visibility } : keyframe
)),
});
};
const markReviewed = (reviewed: boolean) => {
if (!currentDraft || !editingEnabled) return;
const nextDraft: M48ReviewClipDraft = reviewed
? {
...currentDraft,
reviewState: "reviewed",
noObject: currentDraft.tracklets.length === 0,
}
: { ...currentDraft, reviewState: "pending", noObject: null };
const nextDrafts = new Map(drafts);
nextDrafts.set(nextDraft.clipId, nextDraft);
setDrafts(nextDrafts);
setDirty(true);
void save(nextDrafts, "clip-status", { drafts, dirty });
};
const selectClip = (clipId: string) => {
const next = catalog?.clips.find((item) => item.clipId === clipId);
if (!next) return;
setSelectedClipId(next.clipId);
setSequence(next.startSequence);
setSelectedObjectId(null);
setDrawing(false);
};
const selectAdjacentClip = (offset: -1 | 1) => {
const next = catalog?.clips[selectedClipIndex + offset];
if (next) selectClip(next.clipId);
};
const requestClose = () => {
if (dirty) {
if (!window.confirm("Закрыть рабочую область без сохранения черновика?")) return;
onClose();
return;
}
if (session?.complete && session.state !== "frozen") {
setFreezeOpen(true);
return;
}
onClose();
};
return (
<LaboratoryReviewWorkspaceFrame
ariaLabel="M4.8 проверка авторазметки Worker 006"
onClose={requestClose}
interactionEnabled={!freezeOpen}
returnFocusTarget={returnFocusTarget}
toolbar={(
<div className="m48-review-workspace__header">
<div className="m48-review-workspace__topbar">
<div className="m48-review-workspace__topbar-start">
<div className="m48-review-workspace__clip-navigation">
<IconButton
label="Предыдущий клип"
disabled={selectedClipIndex <= 0 || busy}
onClick={() => selectAdjacentClip(-1)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
label="Следующий клип"
disabled={!catalog || selectedClipIndex < 0 || selectedClipIndex >= catalog.clips.length - 1 || busy}
onClick={() => selectAdjacentClip(1)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<FieldFrame label="Выбор клипа" className="m48-review-workspace__clip-field">
<Select
label="Клип для проверки"
value={selectedClipId}
options={(catalog?.clips ?? []).map((item) => ({
value: item.clipId,
label: clipOptionLabel(
item.ordinal,
catalog?.clipCount ?? 0,
item.clipId,
drafts.get(item.clipId)?.reviewState,
),
}))}
disabled={!catalog || busy}
searchable
menuWidth={380}
onChange={selectClip}
/>
</FieldFrame>
{currentDraft && editable ? (
<Checker
className="m48-review-workspace__clip-reviewed"
checked={currentDraft.reviewState === "reviewed"}
disabled={busy}
aria-busy={busy}
aria-label={currentDraft.tracklets.length
? `Клип проверен · ${currentDraft.tracklets.length} объектов`
: "Клип проверен · без объектов"}
label={currentDraft.tracklets.length
? `Проверен · ${currentDraft.tracklets.length} объектов`
: "Проверен · без объектов"}
onChange={markReviewed}
/>
) : null}
</div>
<div className="m48-review-workspace__topbar-end">
<IconButton
label="Добавить объект"
aria-pressed={drawing}
disabled={!editingEnabled || !extentEnabled}
onClick={() => {
setSelectedObjectId(null);
setDrawing((value) => !value);
}}
>
<Icon name="plus" size={16} />
</IconButton>
<IconButton label="Закрыть проверку M4.8" disabled={busy} onClick={requestClose}>
<Icon name="close" size={16} />
</IconButton>
</div>
</div>
{selectedTracklet && editable ? (
<div className="m48-review-workspace__object-tools">
<FieldFrame label="Видимость" className="m48-review-workspace__object-field">
<Select disabled={busy} label="Видимость объекта" value={selectedTracklet.keyframes.filter((keyframe) => keyframe.sequence <= sequence).at(-1)?.visibility ?? "visible"} options={[{ value: "visible", label: "Виден" }, { value: "partial", label: "Виден частично" }, { value: "occluded", label: "Перекрыт" }]} onChange={(value) => updateVisibility(value as M48ReviewTracklet["keyframes"][number]["visibility"])} />
</FieldFrame>
<FieldFrame label="Связь с LiDAR" className="m48-review-workspace__object-field">
<Select disabled={busy || !catalog?.evidenceCapabilities.geometryAssociation} label="Связь объекта с LiDAR" value={selectedTracklet.stateSegments[0]?.geometryAssociation ?? "unknown"} options={[{ value: "associated", label: "Связана" }, { value: "unavailable", label: "Недоступна" }, { value: "ineligible", label: "Не применяется" }, { value: "unknown", label: "Не определена" }]} onChange={(value) => updateTrackState({ geometryAssociation: value as M48ReviewTracklet["stateSegments"][number]["geometryAssociation"] })} />
</FieldFrame>
<FieldFrame label="Актуальность" className="m48-review-workspace__object-field">
<Select disabled={busy || !catalog?.evidenceCapabilities.freshness} label="Актуальность объекта" value={selectedTracklet.stateSegments[0]?.freshness ?? "unavailable"} options={[{ value: "current", label: "Актуальна" }, { value: "held", label: "Удержана" }, { value: "stale", label: "Устарела" }, { value: "unavailable", label: "Недоступна" }]} onChange={(value) => updateTrackState({ freshness: value as M48ReviewTracklet["stateSegments"][number]["freshness"] })} />
</FieldFrame>
<FieldFrame label="Движение" className="m48-review-workspace__object-field">
<Select disabled={busy || !catalog?.evidenceCapabilities.motion} label="Движение объекта" value={selectedTracklet.stateSegments[0]?.motion ?? "unknown"} options={[{ value: "moving", label: "Движется" }, { value: "static", label: "Стоит" }, { value: "unknown", label: "Не определено" }, { value: "unsupported", label: "Не поддерживается" }]} onChange={(value) => updateTrackState({ motion: value as M48ReviewTracklet["stateSegments"][number]["motion"] })} />
</FieldFrame>
<FieldFrame label="Непосредственная опасность" className="m48-review-workspace__object-field m48-review-workspace__object-field--wide">
<Select disabled={busy || !catalog?.evidenceCapabilities.threat} label="Непосредственная опасность объекта" value={selectedTracklet.stateSegments[0]?.threat ?? "unknown"} options={[{ value: "threat", label: "Опасен сейчас" }, { value: "not-threat", label: "Не опасен сейчас" }, { value: "unknown", label: "Не определено" }]} onChange={(value) => updateTrackState({ threat: value as M48ReviewTracklet["stateSegments"][number]["threat"] })} />
</FieldFrame>
<FieldFrame label="Проезд" className="m48-review-workspace__passage-field">
<Checker disabled={busy || !catalog?.evidenceCapabilities.criticalCorridorObstacle} checked={selectedTracklet.stateSegments[0]?.criticalCorridorObstacle ?? false} label="Объезд или запас" onChange={(criticalCorridorObstacle) => updateTrackState({ criticalCorridorObstacle })} />
</FieldFrame>
<IconButton
label="Добавить ещё один объект"
disabled={busy || !extentEnabled}
onClick={() => {
setSelectedObjectId(null);
setDrawing(true);
}}
>
<Icon name="plus" size={16} />
</IconButton>
<IconButton
label="Сохранить изменения объекта"
disabled={!dirty || busy}
aria-busy={busy}
onClick={() => void save(drafts, "object-edit")}
>
<Icon name="save" size={16} />
</IconButton>
<IconButton disabled={busy} label="Удалить объект" onClick={() => {
if (!currentDraft) return;
setCurrentDraft({ ...currentDraft, reviewState: "pending", noObject: null, tracklets: currentDraft.tracklets.filter(({ objectId }) => objectId !== selectedTracklet.objectId) });
setSelectedObjectId(null);
}}><Icon name="trash" size={16} /></IconButton>
<IconButton
label="Закрыть редактор объекта"
disabled={busy}
onClick={() => {
setSelectedObjectId(null);
setDrawing(false);
}}
>
<Icon name="close" size={16} />
</IconButton>
</div>
) : null}
</div>
)}
stage={(
<div className="m48-review-workspace__stage-shell">
{loading ? (
<div className="m48-review-workspace__state" role="status"><span className="busy-indicator" aria-hidden="true" />Загружаем клипы и frozen-candidate seed Worker 006</div>
) : !catalog || !catalog.cameraPlayback || !clip ? (
<div className="m48-review-workspace__state" role="alert"><Icon name="alert" size={18} />{error ?? "M4.8 источник недоступен."}</div>
) : (
<M48BlindClipPlayer
cameraPlayback={catalog.cameraPlayback}
clip={clip}
sequence={sequence}
mode={mode}
cameraVisible={cameraVisible}
tracklets={currentDraft?.tracklets ?? []}
selectedObjectId={selectedObjectId}
editable={editingEnabled && extentEnabled}
drawing={drawing}
spatialFrame={spatial}
spatialLoading={spatialLoading}
spatialError={spatialError}
spatialEvidenceAvailable={spatialEnabled}
onSequenceChange={setSequence}
onDrawingChange={setDrawing}
onSelectedObjectIdChange={setSelectedObjectId}
onTrackletsChange={(tracklets) => {
if (!currentDraft) return;
setCurrentDraft({ ...currentDraft, reviewState: "pending", noObject: null, tracklets });
}}
/>
)}
{catalog ? (
<M48EvidenceModeRail
mode={mode}
cameraVisible={cameraVisible}
spatialAvailable={spatialEnabled}
onModeChange={setMode}
onCameraVisibleChange={setCameraVisible}
/>
) : null}
{catalog ? (
<GlassSurface
className="m48-review-workspace__source-sticker"
tone="strong"
padding="sm"
materialRim={false}
>
<div className="m48-review-workspace__source-heading">
<StatusBadge tone={session ? "success" : "warning"}>
{session
? `Worker 006 · ${session.seedObjectCount.toLocaleString("ru-RU")} авторамок`
: "Загружаем авторазметку"}
</StatusBadge>
<strong>{clip ? `${clip.clipId} · кадр ${sequence}` : "Источник проверяется"}</strong>
</div>
<small>Исправьте авторамки; новая ручная рамка относится только к текущему кадру и не имитирует трекинг.</small>
<small>«Опасность» — немедленная угроза. «Проезд» — статическое ограничение, которое требует объезда или геометрического запаса.</small>
<StatusBadge tone={dirty ? "warning" : session?.state === "frozen" ? "success" : session ? "neutral" : "warning"}>
{busy ? "Сохраняем изменения" : dirty ? "Есть несохранённые изменения" : stateLabel(session)}
</StatusBadge>
{(error || spatialError) ? <StatusBadge tone="danger">{error ?? spatialError}</StatusBadge> : null}
{session?.evidenceSummary ? (
<div className="m48-review-workspace__evidence-summary">
<strong>Результат проверки Worker 006</strong>
<span>Подтверждено: {session.evidenceSummary.confirmedCandidateCount}/{session.evidenceSummary.seedObjectCount}</span>
<span>Исправлено: {session.evidenceSummary.modifiedCandidateCount}</span>
<span>Удалено лишних: {session.evidenceSummary.falsePositiveRemovedCount}</span>
<span>Добавлено пропущенных: {session.evidenceSummary.missedObjectAddedCount}</span>
<small>Это проверка авторазметки, а не независимая контрольная разметка.</small>
</div>
) : null}
</GlassSurface>
) : null}
</div>
)}
overlays={(
<>
<Window
open={freezeOpen}
title="Завершить проверку Worker 006"
subtitle="Будут сохранены исходные авторамки, ваши исправления и итоговая разница. Результат не является независимой контрольной разметкой."
size="md"
closeOnBackdrop={!busy}
closeOnEscape={!busy}
onClose={() => !busy && setFreezeOpen(false)}
footer={<WindowFooterActions><Button disabled={busy} onClick={() => setFreezeOpen(false)}>Отмена</Button><Button variant="accent" disabled={busy || !reviewerId.trim() || !candidateVisible || !classFree} onClick={() => void freeze()}>{busy ? "Завершаем" : "Завершить проверку"}</Button></WindowFooterActions>}
>
<div className="m48-review-workspace__freeze-form">
<TextField label="Кто проверил" value={reviewerId} maxLength={96} placeholder="Имя или ID проверяющего" onChange={(event) => setReviewerId(event.target.value)} />
<Checker checked={candidateVisible} label="Все авторамки Worker 006 просмотрены, найденные ошибки исправлены" onChange={setCandidateVisible} />
<Checker checked={classFree} label="Проверка не назначает объектам семантические классы" onChange={setClassFree} />
</div>
</Window>
<ToastStack items={toasts} onDismiss={(id) => setToasts((current) => current.filter((item) => item.id !== id))} />
</>
)}
/>
);
}
@@ -0,0 +1,97 @@
import {
GlassSurface,
Icon,
IconButton,
} from "@nodedc/ui-react";
export type M48BlindEvidenceMode = "camera" | "3d" | "plan";
interface M48EvidenceModeControlProps {
mode: M48BlindEvidenceMode;
cameraVisible: boolean;
spatialAvailable: boolean;
onModeChange: (mode: M48BlindEvidenceMode) => void;
onCameraVisibleChange: (visible: boolean) => void;
}
export function nextM48CameraVisibility(
mode: M48BlindEvidenceMode,
cameraVisible: boolean,
): boolean {
return mode === "camera" ? true : !cameraVisible;
}
export function nextM48SpatialMode(
mode: M48BlindEvidenceMode,
cameraVisible: boolean,
selected: Exclude<M48BlindEvidenceMode, "camera">,
): M48BlindEvidenceMode {
if (mode !== selected) return selected;
return cameraVisible ? "camera" : mode;
}
export function M48EvidenceModeControls({
mode,
cameraVisible,
spatialAvailable,
onModeChange,
onCameraVisibleChange,
}: M48EvidenceModeControlProps) {
const spatialMode = mode === "camera" ? null : mode;
return (
<div
className="m48-evidence-mode-controls"
role="group"
aria-label="Каналы доказательства"
>
<IconButton
label={cameraVisible ? "Скрыть правую камеру" : "Показать правую камеру"}
aria-pressed={cameraVisible}
disabled={spatialMode === null}
onClick={() => onCameraVisibleChange(
nextM48CameraVisibility(mode, cameraVisible),
)}
>
<Icon name="video" size={16} />
</IconButton>
<IconButton
label={spatialMode === "3d" ? "Скрыть 3D" : "Показать 3D"}
aria-pressed={spatialMode === "3d"}
disabled={!spatialAvailable || (!cameraVisible && spatialMode === "3d")}
onClick={() => {
if (!spatialAvailable) return;
onModeChange(nextM48SpatialMode(mode, cameraVisible, "3d"));
}}
>
<span className="m48-evidence-mode-controls__text" aria-hidden="true">3D</span>
</IconButton>
<IconButton
label={spatialMode === "plan" ? "Скрыть план" : "Показать план"}
aria-pressed={spatialMode === "plan"}
disabled={!spatialAvailable || (!cameraVisible && spatialMode === "plan")}
onClick={() => {
if (!spatialAvailable) return;
onModeChange(nextM48SpatialMode(mode, cameraVisible, "plan"));
}}
>
<Icon name="plan" size={16} />
</IconButton>
</div>
);
}
export function M48EvidenceModeRail(props: M48EvidenceModeControlProps) {
return (
<GlassSurface
className="m48-evidence-mode-rail"
tone="strong"
radius="pill"
padding="sm"
materialRim={false}
role="toolbar"
aria-label="Режимы CAMERA, 3D и план"
>
<M48EvidenceModeControls {...props} />
</GlassSurface>
);
}
@@ -0,0 +1,73 @@
import { useCallback, useEffect, useRef, useState, type ReactNode } from "react";
import {
fetchM48GateStatus,
type M48GateStatus,
} from "../../../core/laboratory/m48ObjectCentricQuality";
import type { LaboratoryAnnotationAction } from "../../contracts";
import { M48CorrectionWorkspace } from "./M48BlindReviewWorkspace";
export function useM48ReviewCapability({
selectedWorkId,
initialGate,
onActionChange,
}: {
selectedWorkId: string;
initialGate: M48GateStatus | null;
onActionChange: (action: LaboratoryAnnotationAction | null) => void;
}): { workspace: ReactNode; active: boolean } {
const [gate, setGate] = useState(initialGate);
const [open, setOpen] = useState(false);
const ownsAction = useRef(false);
const actionTrigger = useRef<HTMLElement | null>(null);
useEffect(() => setGate(initialGate), [initialGate]);
const refresh = useCallback(() => {
if (!gate) return;
void fetchM48GateStatus(gate.packId).then(setGate).catch(() => undefined);
}, [gate]);
useEffect(() => {
if (selectedWorkId !== "m48-object-centric-quality" || !gate) {
if (ownsAction.current) {
onActionChange(null);
ownsAction.current = false;
}
setOpen(false);
return;
}
ownsAction.current = true;
onActionChange({
label: open
? "Рабочая область открыта"
: "Проверить Worker 006",
disabled: Boolean(open) || gate.evaluated,
onClick: () => {
actionTrigger.current = document.activeElement instanceof HTMLElement
? document.activeElement
: null;
setOpen(true);
},
});
return () => {
if (ownsAction.current) {
onActionChange(null);
ownsAction.current = false;
}
};
}, [gate, onActionChange, open, selectedWorkId]);
if (!gate || !open) return { workspace: null, active: false };
return {
active: true,
workspace: (
<M48CorrectionWorkspace
gate={gate}
returnFocusTarget={actionTrigger.current}
onClose={() => setOpen(false)}
onChanged={refresh}
/>
),
};
}
@@ -0,0 +1,126 @@
import { useEffect, useRef, useState } from "react";
import {
fetchM48ReviewSpatialFrame,
type M48ReviewClipSource,
type M48ReviewSpatialFrame,
} from "../../../core/laboratory/m48ObjectCentricQuality";
export const M48_SPATIAL_PREFETCH_FRAME_COUNT = 14;
export const M48_SPATIAL_CACHE_FRAME_LIMIT = 24;
export function m48SpatialPlaybackWindow(
frames: readonly { sequence: number }[],
sequence: number,
frameCount = M48_SPATIAL_PREFETCH_FRAME_COUNT,
): readonly number[] {
if (!frames.length || frameCount <= 0) return [];
const currentIndex = Math.max(0, frames.findIndex((frame) => frame.sequence === sequence));
const count = Math.min(frameCount, frames.length);
return Array.from({ length: count }, (_, offset) => (
frames[(currentIndex + offset) % frames.length]!.sequence
));
}
export function trimM48SpatialPlaybackCache<T>(
cache: Map<number, T>,
protectedSequences: readonly number[],
limit = M48_SPATIAL_CACHE_FRAME_LIMIT,
): void {
const protectedSet = new Set(protectedSequences);
for (const key of cache.keys()) {
if (cache.size <= limit) return;
if (!protectedSet.has(key)) cache.delete(key);
}
for (const key of cache.keys()) {
if (cache.size <= limit) return;
cache.delete(key);
}
}
function errorMessage(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "Spatial evidence для текущего кадра недоступно.";
}
function aborted(error: unknown): boolean {
return error instanceof Error && error.name === "AbortError";
}
export function useM48SpatialClipPlayback({
packId,
clip,
sequence,
enabled,
}: {
packId: string;
clip: M48ReviewClipSource | null;
sequence: number;
enabled: boolean;
}): {
frame: M48ReviewSpatialFrame | null;
loading: boolean;
error: string | null;
} {
const cacheRef = useRef(new Map<number, M48ReviewSpatialFrame>());
const errorsRef = useRef(new Map<number, string>());
const inFlightRef = useRef(new Map<number, Promise<void>>());
const controllerRef = useRef<AbortController | null>(null);
const generationRef = useRef(0);
const [, setRevision] = useState(0);
const sourceKey = enabled && clip ? `${packId}:${clip.clipId}` : null;
useEffect(() => {
generationRef.current += 1;
controllerRef.current?.abort();
controllerRef.current = sourceKey ? new AbortController() : null;
cacheRef.current.clear();
errorsRef.current.clear();
inFlightRef.current.clear();
setRevision((value) => value + 1);
return () => controllerRef.current?.abort();
}, [sourceKey]);
useEffect(() => {
const controller = controllerRef.current;
if (!sourceKey || !clip || !controller || controller.signal.aborted) return;
const generation = generationRef.current;
const wanted = m48SpatialPlaybackWindow(clip.frames, sequence);
const load = (nextSequence: number): Promise<void> => {
const existing = inFlightRef.current.get(nextSequence);
if (existing) return existing;
if (cacheRef.current.has(nextSequence)) return Promise.resolve();
const request = fetchM48ReviewSpatialFrame(
packId,
clip.clipId,
nextSequence,
{ signal: controller.signal },
).then((next) => {
if (controller.signal.aborted || generation !== generationRef.current) return;
cacheRef.current.set(nextSequence, next);
errorsRef.current.delete(nextSequence);
trimM48SpatialPlaybackCache(cacheRef.current, wanted);
setRevision((value) => value + 1);
}).catch((caught: unknown) => {
if (controller.signal.aborted || aborted(caught) || generation !== generationRef.current) return;
errorsRef.current.set(nextSequence, errorMessage(caught));
setRevision((value) => value + 1);
}).finally(() => {
if (generation === generationRef.current) inFlightRef.current.delete(nextSequence);
});
inFlightRef.current.set(nextSequence, request);
return request;
};
for (const nextSequence of wanted) void load(nextSequence);
}, [clip, packId, sequence, sourceKey]);
const frame = sourceKey ? cacheRef.current.get(sequence) ?? null : null;
return {
frame,
loading: Boolean(sourceKey && !frame && !errorsRef.current.has(sequence)),
error: sourceKey ? errorsRef.current.get(sequence) ?? null : null,
};
}
@@ -63,6 +63,20 @@ interface KnownWorkDefinition {
const rig = (rigLabel: string): string => rigLabel.trim() || "Сенсорный риг";
const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`>, KnownWorkDefinition>> = {
"m48-object-centric-quality": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + prediction-free spatial evidence`,
experimentId: "m48-object-centric-source-quality",
experimentName: "RAVNOVES00 class-free object-centric source quality",
variantName: "M4.8 · Worker 006 assisted correction → evidence delta",
},
"m48-small-static-passage-regression": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + prediction-free spatial evidence`,
experimentId: "m48-small-static-passage-regression",
experimentName: "M4.8 · small static passage regression",
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
},
"m47-reference-graph-shadow": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
@@ -19,6 +19,8 @@ function mergeResults(
): AdvancedLaboratoryResults {
return {
m47Graph: next.m47Graph ?? current.m47Graph,
m48: next.m48 ?? current.m48,
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
m4Threat: next.m4Threat ?? current.m4Threat,
l3: next.l3 ?? current.l3,
l31: next.l31 ?? current.l31,
@@ -111,7 +113,14 @@ export function useAdvancedLaboratoryCatalog({
|| advancedLaboratoryResultAvailable(selectedWorkId, results)
) return;
const indexedResultId = index.find((item) => item.workId === selectedWorkId)?.resultId;
if (selectedWorkId === "m47-reference-graph-shadow" && !indexedResultId) return;
if (
[
"m47-reference-graph-shadow",
"m48-object-centric-quality",
"m48-small-static-passage-regression",
].includes(selectedWorkId)
&& !indexedResultId
) return;
const controller = new AbortController();
setLoadingWorkId(selectedWorkId);
setFailedWorkId(null);
@@ -963,6 +963,46 @@ test("LAB entry defaults atomically to the freshest pipeline, experiment and run
});
});
test("M4.8R1 stays in the current pipeline as a separate experiment and run", () => {
const catalog = buildLaboratoryCatalog({
rigLabel: "K1",
knownWorks: [],
advancedIndex: [
{
workId: "m48-object-centric-quality",
resultId: `m48-object-quality-pack-${"8".repeat(64)}`,
createdAtUtc: "2026-08-24T12:00:00Z",
},
{
workId: "m48-small-static-passage-regression",
resultId: `m48-small-static-passage-regression-${"9".repeat(64)}`,
createdAtUtc: "2026-08-24T18:30:00Z",
},
],
publishedWorks: [],
});
const profiles = buildLaboratoryProfiles(catalog);
assert.deepEqual(profiles.map(({ id }) => id), [
"rig-dual-evidence-virtual-corridor-v1",
]);
assert.deepEqual(
experimentOptionsForProfile(profiles[0].id, catalog).map(({ id }) => id),
[
"m48-small-static-passage-regression",
"m48-object-centric-source-quality",
],
);
assert.deepEqual(
freshestLaboratorySelection(catalog),
{
profileId: "rig-dual-evidence-virtual-corridor-v1",
experimentId: "m48-small-static-passage-regression",
workId: "m48-small-static-passage-regression",
},
);
});
test("E46E is exposed as the newest independent NVIDIA pipeline", () => {
const catalog = buildLaboratoryCatalog({
rigLabel: "K1",
@@ -264,6 +264,11 @@ test("LAB product surface has a compact canonical summary and no roadmap footer"
]);
assert.match(presentationSource, /export function LaboratorySummary/);
assert.match(presentationSource, /const \[expanded, setExpanded\] = useState\(false\)/);
assert.match(presentationSource, /aria-expanded=\{expanded\}/);
assert.match(presentationSource, /className="laboratory-summary__details" hidden=\{!expanded\}/);
assert.match(presentationSource, /className="laboratory-summary__actions"/);
assert.match(presentationSource, /name="chevron-down"/);
assert.match(presentationSource, /export function LaboratoryWorkTemplate/);
assert.match(
presentationSource,
@@ -0,0 +1,693 @@
import assert from "node:assert/strict";
import { readFileSync, readdirSync } from "node:fs";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let decodeM48GateStatus;
let decodeM48ReviewSourceCatalog;
let decodeM48CorrectionSession;
let decodeM48SpatialFrame;
let decodeM48QualityResult;
let decodeM48FailureAtlas;
let decodeM48FailureCase;
let assertM48BlindPayload;
let interpolateM48Extent;
let createM48Tracklet;
let nextM48ObjectId;
let laboratoryMetricLegendEntries;
let nearestLaboratoryRecordedClipFrame;
let laboratoryRecordedClipEndExclusiveNs;
let m48SpatialPlaybackWindow;
let trimM48SpatialPlaybackCache;
let nextM48CameraVisibility;
let nextM48SpatialMode;
const packId = `m48-object-quality-pack-${"a".repeat(64)}`;
before(async () => {
server = await createServer({ appType: "custom", logLevel: "silent", server: { middlewareMode: true } });
({
decodeM48GateStatus,
decodeM48ReviewSourceCatalog,
decodeM48CorrectionSession,
decodeM48SpatialFrame,
decodeM48QualityResult,
decodeM48FailureAtlas,
decodeM48FailureCase,
assertM48BlindPayload,
} = await server.ssrLoadModule("/src/core/laboratory/m48ObjectCentricQuality.ts"));
({
interpolateM48Extent,
createM48Tracklet,
nextM48ObjectId,
} = await server.ssrLoadModule("/src/workspaces/laboratory/annotation/M48BlindClipPlayer.tsx"));
({
nearestLaboratoryRecordedClipFrame,
laboratoryRecordedClipEndExclusiveNs,
} = await server.ssrLoadModule(
"/src/components/laboratory/LaboratoryRecordedClipPlayer.tsx",
));
({ laboratoryMetricLegendEntries } = await server.ssrLoadModule(
"/src/components/laboratory/LaboratoryMetricEvidenceScene.tsx",
));
({
m48SpatialPlaybackWindow,
trimM48SpatialPlaybackCache,
} = await server.ssrLoadModule(
"/src/workspaces/laboratory/annotation/useM48SpatialClipPlayback.ts",
));
({
nextM48CameraVisibility,
nextM48SpatialMode,
} = await server.ssrLoadModule(
"/src/workspaces/laboratory/annotation/M48EvidenceModeControls.tsx",
));
});
after(async () => server?.close());
function authority() {
return {
mode: "replay-simulated",
physical_live: false,
commands_enabled: false,
actuation_allowed: false,
navigation_or_safety_accepted: false,
};
}
function source() {
const clips = Array.from({ length: 20 }, (_, index) => {
const start = index * 2 + 1;
return {
clip_id: `clip-${String(index + 1).padStart(2, "0")}`,
start_sequence: start,
end_sequence: start + 1,
frames: [start, start + 1].map((sequence) => ({
sequence,
source_time_ns: (sequence - 1) * 100_000_000,
camera_fragment_sha256: String(index % 10).repeat(64),
camera_url: `/api/v1/laboratory/m48/packs/${packId}/source/clips/clip-${String(index + 1).padStart(2, "0")}/frames/${sequence}/camera`,
spatial_url: `/api/v1/laboratory/m48/packs/${packId}/source/clips/clip-${String(index + 1).padStart(2, "0")}/frames/${sequence}/spatial`,
})),
};
});
return {
schema_version: "missioncore.m48-neutral-object-review-source/v2",
pack_id: packId,
state: "prediction-blind-neutral-source-projection",
contract: {
contract_id: "m48-class-free-object-tracklet/v1",
label_unit: "clip-local-object-tracklet",
semantic_classes_allowed: false,
extent: "normalized-xyxy-sparse-keyframes",
extent_interpolation: "linear-between-bounding-keyframes",
visibility: ["occluded", "partial", "visible"],
state_segments: {
coverage: "contiguous-full-tracklet-lifetime",
geometry_association: ["associated", "ineligible", "unavailable", "unknown"],
freshness: ["current", "held", "stale", "unavailable"],
motion: ["moving", "static", "unknown", "unsupported"],
threat: ["not-threat", "threat", "unknown"],
critical_corridor_obstacle: "boolean",
},
},
camera_playback: {
schema_version: "missioncore.laboratory-recorded-clip-camera/v1",
source_id: "recorded.camera.test",
label: "Записанная RIGHT камера",
manifest_url: "/api/v1/observation-sessions/recorded-session/media/recorded-video-test/manifest",
manifest_generation_sha256: "f".repeat(64),
byte_length: 123456,
media_type: "video/mp4",
timeline_start_seconds: 0,
timeline_end_seconds: 4,
segment_count: 40,
seekable: true,
synchronization: "host-arrival-best-effort",
transport: "recorded-fmp4-manifest",
fragment_binding: "pack-frozen-sha256-verified",
},
clips,
clip_count: clips.length,
frame_count: 40,
strata_included: false,
split_included: false,
candidate_identity_included: false,
frozen_predictions_included: false,
model_scores_included: false,
semantic_class_task_included: false,
evidence_capabilities: {
state: "prediction-free-spatial-evidence-available",
camera_epoch_time: true,
current_point_cloud_body_xyz_m: true,
rig: true,
virtual_corridor: true,
raw_lidar: false,
graph_output: false,
graph_boxes_ids_scores: false,
label_authority: {
obstacle_presence_and_extent: true,
geometry_association: true,
freshness: true,
motion: true,
threat: true,
critical_corridor_obstacle: true,
},
fail_closed_reason: null,
},
access: "prediction-free-strata-free-source-read-only",
};
}
test("M4.8 pack status stays fail-closed and command-free", () => {
const result = decodeM48GateStatus({
schema_version: "missioncore.m48-object-quality-pack-status/v1",
pack_id: packId,
created_at_utc: "2026-08-24T10:00:00Z",
state: "prepared",
metrics: { clip_count: 20, frame_count: 1020, seed_object_count: 5236, correction_state: "not-started", correction_reviewed_clip_count: 0, correction_complete: false, review_slot_count: 0, frozen_reviewer_count: 0, required_frozen_reviewer_count: 2 },
decision: { review_collection_ready: true, two_distinct_reviews_frozen: false, adjudication_unlocked: false, adjudication_frozen: false, evaluated: false, next_action: "freeze two reviews" },
truth_seal_id: null,
quality_result_id: null,
blindness: { candidate_identity_included: false, frozen_predictions_included: false, model_scores_included: false, semantic_class_task_included: false, strata_included: false },
authority: authority(),
access: "neutral-workflow-status-read-only",
});
assert.equal(result.packId, packId);
assert.equal(result.authority.commandsEnabled, false);
assert.equal(result.correctionState, "not-started");
assert.equal(result.seedObjectCount, 5236);
assert.equal(result.requiredFrozenReviewerCount, 2);
});
test("blind source admits fragment identity and structurally rejects candidate material", () => {
const result = decodeM48ReviewSourceCatalog(source());
assert.equal(result.clips.length, 20);
assert.equal(result.cameraPlayback.segmentCount, 40);
assert.equal(result.cameraPlayback.manifestGenerationSha256, "f".repeat(64));
assert.equal(result.clips[0].frames[0].cameraFragmentSha256.length, 64);
assert.equal(result.evidenceCapabilities.currentPointCloudBodyXyzM, true);
for (const leak of [
{ predictions: [] },
{ strata: ["no-object"] },
{ model: { id: "candidate" } },
{ graph: [] },
{ split: "validation" },
{ semantic_class: "car" },
]) assert.throws(() => assertM48BlindPayload(leak), /blind|candidate|stratum/i);
const staleName = source();
staleName.clips[0].frames[0].camera_frame_sha256 = staleName.clips[0].frames[0].camera_fragment_sha256;
delete staleName.clips[0].frames[0].camera_fragment_sha256;
assert.throws(() => decodeM48ReviewSourceCatalog(staleName), /состав полей/);
});
test("candidate-assisted correction admits frozen boxes without claiming independent truth", () => {
const candidate = {
schema_version: "missioncore.m48-assisted-object-correction-session/v1",
pack_id: packId,
session_id: `m48-correction-session-${"b".repeat(64)}`,
title: "Worker 006 correction",
revision: 0,
state: "draft",
created_at_utc: "2026-08-24T12:00:00Z",
updated_at_utc: "2026-08-24T12:00:00Z",
clips: [{
clip_id: "clip-01",
start_sequence: 1,
end_sequence: 2,
review_state: "pending",
no_object: null,
tracklets: [{
object_id: "proposal-0-0",
first_sequence: 1,
last_sequence: 1,
keyframes: [{ sequence: 1, extent_xyxy: [0.1, 0.2, 0.3, 0.4], visibility: "visible" }],
state_segments: [{ start_sequence: 1, end_sequence: 1, geometry_association: "associated", freshness: "current", motion: "unknown", threat: "unknown", critical_corridor_obstacle: false }],
notes: null,
}],
notes: null,
}],
progress: { reviewed_clip_count: 0, clip_count: 1, complete: false },
seed_summary: { worker_id: "006", clip_count: 1, frame_count: 2, object_count: 1, prediction_rows_sha256: "c".repeat(64) },
evidence_summary: null,
reviewer_id: null,
submitted_at_utc: null,
submission_sha256: null,
assistance: { mode: "frozen-candidate-seeded", candidate_predictions_seen: true, model_scores_seen: false, semantic_class_task_seen: false, independent_truth_eligible: false },
authority: authority(),
access: "capability-protected-candidate-assisted-correction",
};
const decoded = decodeM48CorrectionSession(candidate);
assert.equal(decoded.seedWorkerId, "006");
assert.equal(decoded.seedObjectCount, 1);
assert.equal(decoded.clips[0].tracklets[0].objectId, "proposal-0-0");
candidate.assistance.independent_truth_eligible = true;
assert.throws(() => decodeM48CorrectionSession(candidate), /assistance|truth/i);
});
test("spatial frame accepts only current points, virtual rig and corridor", () => {
const result = decodeM48SpatialFrame({
schema_version: "missioncore.m48-neutral-object-review-spatial-frame/v1",
pack_id: packId,
clip_id: "clip-01",
sequence: 1,
source_time_ns: 0,
point_cloud_body_xyz_m: [[1, 0, 0.25]],
rig: { profile_id: "virtual-rig", length_m: 1, width_m: 0.6, lidar_reference: "rear", nominal_sensor_height_m: 1.25, physical_mount_claimed: false },
corridor: { profile_id: "corridor", forward_length_m: 8, rear_margin_m: 0.2, lateral_clearance_m: 0.25, half_width_m: 0.55, prediction_horizon_seconds: 5 },
occupied_voxel_size_m: 0.2,
source_available: true,
body_frame_available: true,
candidate_identity_included: false,
graph_boxes_ids_scores_included: false,
frozen_predictions_included: false,
strata_included: false,
authority: authority(),
access: "prediction-free-current-spatial-evidence-read-only",
});
assert.deepEqual(result.pointCloudBodyXyzM, [[1, 0, 0.25]]);
assert.equal(result.sourceAvailable, true);
assert.equal(result.bodyFrameAvailable, true);
assert.equal(result.corridor.forwardLengthM, 8);
const unavailable = {
schema_version: "missioncore.m48-neutral-object-review-spatial-frame/v1",
pack_id: packId,
clip_id: "clip-01",
sequence: 1,
source_time_ns: 0,
point_cloud_body_xyz_m: [[1, 0, 0.25]],
rig: { profile_id: "virtual-rig", length_m: 1, width_m: 0.6, lidar_reference: "rear", nominal_sensor_height_m: 1.25, physical_mount_claimed: false },
corridor: { profile_id: "corridor", forward_length_m: 8, rear_margin_m: 0.2, lateral_clearance_m: 0.25, half_width_m: 0.55, prediction_horizon_seconds: 5 },
occupied_voxel_size_m: 0.2,
source_available: true,
body_frame_available: false,
candidate_identity_included: false,
graph_boxes_ids_scores_included: false,
frozen_predictions_included: false,
strata_included: false,
authority: authority(),
access: "prediction-free-current-spatial-evidence-read-only",
};
assert.throws(() => decodeM48SpatialFrame(unavailable), /unavailable spatial frame/);
});
test("tracklet extents interpolate on the shared clip timeline", () => {
const tracklet = {
objectId: "object-01",
firstSequence: 10,
lastSequence: 20,
keyframes: [
{ sequence: 10, extentXyxy: [0.1, 0.2, 0.3, 0.4], visibility: "visible" },
{ sequence: 20, extentXyxy: [0.2, 0.3, 0.4, 0.5], visibility: "visible" },
],
stateSegments: [],
notes: null,
};
const midpoint = interpolateM48Extent(tracklet, 15);
assert.ok(midpoint.every((value, index) => Math.abs(value - [0.15, 0.25, 0.35, 0.45][index]) < 1e-12));
assert.equal(interpolateM48Extent(tracklet, 9), null);
});
test("manual correction is frame-local, fails closed without spatial authority and never reuses a deleted id", () => {
const clip = decodeM48ReviewSourceCatalog(source()).clips[0];
const tracklet = createM48Tracklet("object-01", clip, [0.1, 0.2, 0.3, 0.4], false, clip.endSequence);
assert.equal(tracklet.firstSequence, clip.endSequence);
assert.equal(tracklet.lastSequence, clip.endSequence);
assert.equal(tracklet.stateSegments[0].startSequence, clip.endSequence);
assert.equal(tracklet.stateSegments[0].endSequence, clip.endSequence);
assert.equal(interpolateM48Extent(tracklet, clip.startSequence), null);
assert.deepEqual(interpolateM48Extent(tracklet, clip.endSequence), [0.1, 0.2, 0.3, 0.4]);
assert.equal(tracklet.stateSegments[0].geometryAssociation, "unavailable");
assert.equal(tracklet.stateSegments[0].motion, "unsupported");
assert.equal(nextM48ObjectId([
tracklet,
{ ...tracklet, objectId: "object-03" },
]), "object-02");
});
test("shared recorded clip clock selects exact frames and one stable loop boundary", () => {
const frames = [
{ sequence: 11, sourceTimeNs: 1_000_000_000 },
{ sequence: 12, sourceTimeNs: 1_100_000_000 },
{ sequence: 13, sourceTimeNs: 1_200_000_000 },
];
assert.equal(nearestLaboratoryRecordedClipFrame(frames, 1_049_000_000).sequence, 11);
assert.equal(nearestLaboratoryRecordedClipFrame(frames, 1_051_000_000).sequence, 12);
assert.equal(laboratoryRecordedClipEndExclusiveNs(frames), 1_300_000_000);
});
test("M4.8 camera and spatial visibility are independent without an empty viewer", () => {
assert.equal(nextM48CameraVisibility("camera", true), true);
assert.equal(nextM48CameraVisibility("3d", true), false);
assert.equal(nextM48CameraVisibility("3d", false), true);
assert.equal(nextM48SpatialMode("camera", true, "3d"), "3d");
assert.equal(nextM48SpatialMode("3d", true, "3d"), "camera");
assert.equal(nextM48SpatialMode("3d", false, "3d"), "3d");
assert.equal(nextM48SpatialMode("3d", false, "plan"), "plan");
});
test("M4.8 spatial playback prefetches across the loop and remains bounded", () => {
const frames = [11, 12, 13, 14, 15].map((sequence) => ({ sequence }));
assert.deepEqual(m48SpatialPlaybackWindow(frames, 14, 4), [14, 15, 11, 12]);
const cache = new Map(Array.from({ length: 30 }, (_, index) => [index + 1, index]));
trimM48SpatialPlaybackCache(cache, [28, 29, 30, 1], 24);
assert.equal(cache.size, 24);
for (const sequence of [28, 29, 30, 1]) assert.equal(cache.has(sequence), true);
});
test("post-seal result and atlas reveal bounded graph material only after evaluation", () => {
const resultId = `m48-object-quality-result-${"b".repeat(64)}`;
const truthId = `m48-object-truth-seal-${"c".repeat(64)}`;
const metricNames = [
"terminal_outcome_accounting",
"false_free_space_claims",
"obstacle_presence_precision",
"obstacle_presence_recall",
"critical_corridor_obstacle_recall",
"geometry_association_correctness",
"freshness_correctness",
"motion_decision_correctness",
"critical_threat_not_threat",
"unknown_prediction_count",
"failure_case_count",
];
const metrics = Object.fromEntries(metricNames.map((name) => [name, name.endsWith("_count") || name === "false_free_space_claims" ? 0 : 1]));
const quality = decodeM48QualityResult({
schema_version: "missioncore.m48-object-centric-quality-result-view/v1",
result_id: resultId,
pack_id: packId,
truth_seal_id: truthId,
created_at_utc: "2026-08-24T12:00:00Z",
status: "accepted-object-centric-source-quality",
accepted: true,
metrics,
gates: { obstacle_presence_precision: true },
unknown_causes: {},
prediction_material_release: "post-adjudication-seal-evaluation-only",
authority: authority(),
ground_truth: false,
access: "evaluated-object-quality-summary-read-only",
});
assert.equal(quality.accepted, true);
const caseId = `m48-failure-${"d".repeat(64)}`;
const atlas = decodeM48FailureAtlas({
schema_version: "missioncore.m48-object-quality-failure-atlas-view/v1",
result_id: resultId,
cases: [{ schema_version: "missioncore.m48-object-quality-failure/v1", failure_case_id: caseId, clip_id: "clip-01", split: "validation", sequence: 1, causes: ["presence-false-negative"], severity: "high", terminal_outcome: "delivered", unmatched_prediction_ids: [], unmatched_truth_object_ids: ["object-01"] }],
case_count: 1,
prediction_material_release: "post-adjudication-seal-evaluation-only",
authority: authority(),
access: "evaluated-bounded-failure-atlas-read-only",
});
assert.equal(atlas[0].caseId, caseId);
assert.equal(atlas[0].split, "validation");
const failure = decodeM48FailureCase({
schema_version: "missioncore.m48-object-quality-failure-case-view/v1",
result_id: resultId,
case: { failure_case_id: caseId, clip_id: "clip-01", split: "validation", sequence: 1, causes: ["presence-false-negative"], severity: "high" },
frame: { sequence: 1, source_time_ns: 0, camera_fragment_sha256: "e".repeat(64), camera_url: `/api/v1/laboratory/m48/packs/${packId}/source/clips/clip-01/frames/1/camera`, spatial_url: null },
truth: [{ object_id: "object-01", extent_xyxy: [0.1, 0.1, 0.3, 0.4], geometry_association: "associated", freshness: "current", motion: "static", threat: "threat" }],
graph: [],
prediction_material_release: "post-adjudication-seal-evaluation-only",
authority: authority(),
access: "evaluated-failure-case-read-only",
});
assert.equal(failure.truth[0].objectId, "object-01");
assert.equal(failure.frame.cameraFragmentSha256, "e".repeat(64));
});
test("M4.8 evidence keeps the shared viewer stage stretched over the visual frame", () => {
const stylesheet = readFileSync(
new URL("../src/styles/laboratory-recorded-clip-player.css", import.meta.url),
"utf8",
);
const player = readFileSync(
new URL("../src/workspaces/laboratory/annotation/M48BlindClipPlayer.tsx", import.meta.url),
"utf8",
);
const shared = readFileSync(
new URL("../src/components/laboratory/LaboratoryRecordedClipPlayer.tsx", import.meta.url),
"utf8",
);
const visual = readFileSync(
new URL("../src/workspaces/laboratory/M48FailureAtlasVisual.tsx", import.meta.url),
"utf8",
);
const evidenceViewer = readFileSync(
new URL("../src/components/laboratory/LaboratoryEvidenceViewer.tsx", import.meta.url),
"utf8",
);
const laboratoryStyles = readFileSync(
new URL("../src/styles/laboratory-evidence-viewer.css", import.meta.url),
"utf8",
);
const modeControls = readFileSync(
new URL("../src/workspaces/laboratory/annotation/M48EvidenceModeControls.tsx", import.meta.url),
"utf8",
);
assert.match(
stylesheet,
/\.laboratory-recorded-clip-player__camera\s*>\s*\.recorded-media-player\s*\{[^}]*position:\s*absolute/s,
);
assert.match(player, /<LaboratoryRecordedClipPlayer/);
assert.doesNotMatch(player, /fetch\(|image\/jpeg|createM48LatestFrameLoader|Следующий camera frame/);
assert.match(shared, /missioncore\.laboratory-recorded-clip-viewer\/v1/);
assert.match(shared, /<RecordedFmp4Player/);
assert.match(shared, /<SplitPane/);
assert.equal((shared.match(/<RecordedFmp4Player/g) ?? []).length, 1);
assert.match(shared, /orientation="vertical"/);
assert.match(shared, /resizable=\{companionVisible\}/);
assert.match(shared, /separatorLabel="Изменить размер 3D\/ПЛАН и правой камеры"/);
assert.match(shared, /segmentSequence=\{frame\?\.sequence\}/);
assert.match(player, /cameraPresentation=\{spatialVisible/);
assert.match(player, /effectiveCameraVisible \? "companion" : "hidden"/);
assert.match(player, /cameraVisible: boolean/);
assert.match(player, /continuousPlayback/);
assert.doesNotMatch(player, /continuousPlayback=\{mode === "camera"\}/);
assert.match(visual, /chromeLayout="stacked"/);
assert.match(visual, /modeControlsVisible=\{false\}/);
assert.match(visual, /<M48EvidenceModeRail/);
assert.match(modeControls, /<GlassSurface/);
assert.match(modeControls, /className="m48-evidence-mode-rail"/);
assert.match(modeControls, /radius="pill"/);
assert.match(modeControls, /padding="sm"/);
assert.match(modeControls, /materialRim=\{false\}/);
assert.match(modeControls, /<Icon name="video" size=\{16\}/);
assert.match(modeControls, /<Icon name="plan" size=\{16\}/);
assert.match(modeControls, />3D<\/span>/);
assert.doesNotMatch(modeControls, /<SegmentedControl|<Button/);
assert.doesNotMatch(modeControls, /value: "camera"[\s\S]*value: "3d"/);
assert.match(evidenceViewer, /chromeLayout\?: "overlay" \| "stacked"/);
assert.match(evidenceViewer, /laboratory-evidence-viewer__header/);
assert.match(
laboratoryStyles,
/data-chrome-layout="stacked"[\s\S]*?grid-template-rows:\s*auto minmax\(0, 1fr\) auto/,
);
assert.match(
stylesheet,
/data-chrome-layout="stacked"[\s\S]*?laboratory-recorded-clip-player\s*\{[\s\S]*?gap:\s*0/,
);
assert.doesNotMatch(stylesheet, /grid-template-columns:[^;]*31%/);
assert.doesNotMatch(
stylesheet,
/border-(?:left|top):\s*1px solid var\(--nodedc-glass-outline\)/,
);
assert.match(
stylesheet,
/nodedc-split-pane__separator::before\s*\{[^}]*background:\s*transparent/s,
);
assert.match(
laboratoryStyles,
/laboratory-evidence-viewer__header\s*\{[^}]*border:\s*0/s,
);
});
test("LAB workspaces cannot fork the frozen recorded clip transport", () => {
const root = new URL("../src/workspaces/laboratory/", import.meta.url);
const sourceFiles = readdirSync(root, { recursive: true })
.filter((name) => /\.(?:ts|tsx)$/.test(String(name)));
for (const name of sourceFiles) {
const sourceText = readFileSync(new URL(String(name), root), "utf8");
assert.doesNotMatch(
sourceText,
/new\s+MediaSource|requestVideoFrameCallback|URL\.createObjectURL|image\/jpeg|setTimeout\s*\(|<RecordedFmp4Player/,
`${name} forks missioncore.laboratory-recorded-clip-viewer/v1`,
);
}
});
test("M4.8 review and adjudication share one focus-owning workspace frame", () => {
const review = readFileSync(
new URL("../src/workspaces/laboratory/annotation/M48BlindReviewWorkspace.tsx", import.meta.url),
"utf8",
);
const adjudication = readFileSync(
new URL("../src/workspaces/laboratory/annotation/M48AdjudicationWorkspace.tsx", import.meta.url),
"utf8",
);
const frame = readFileSync(
new URL("../src/components/laboratory/LaboratoryReviewWorkspaceFrame.tsx", import.meta.url),
"utf8",
);
const frameStyles = readFileSync(
new URL("../src/styles/laboratory-review-workspace.css", import.meta.url),
"utf8",
);
const m48Styles = readFileSync(
new URL("../src/styles/m48-object-centric-quality.css", import.meta.url),
"utf8",
);
const recordedStyles = readFileSync(
new URL("../src/styles/laboratory-recorded-clip-player.css", import.meta.url),
"utf8",
);
for (const workspace of [review, adjudication]) {
assert.match(workspace, /LaboratoryReviewWorkspaceFrame/);
assert.doesNotMatch(workspace, /createPortal|className="m48-review-workspace"/);
assert.doesNotMatch(
workspace,
/@rerun-io|RerunViewer|Blueprint|view_id|blueprint_id/,
"M4.8 must reuse shared viewers instead of defining a per-LAB Rerun layout",
);
}
assert.match(frame, /document\.body\.style\.overflow = "hidden"/);
assert.match(frame, /event\.key === "Escape"/);
assert.match(frame, /keepFocusInside/);
assert.match(frame, /returnFocusTarget\?\.isConnected/);
assert.match(frame, /requestAnimationFrame\(\(\) => target\?\.focus\(\)\)/);
const player = readFileSync(
new URL("../src/workspaces/laboratory/annotation/M48BlindClipPlayer.tsx", import.meta.url),
"utf8",
);
assert.match(player, /spatialFrame\.bodyFrameAvailable/);
assert.match(player, /LiDAR в системе координат корпуса для этого кадра недоступен/);
assert.match(player, /Number\(effectiveCameraVisible\) \+ Number\(spatialVisible\)/);
assert.match(player, /data-spatial-sequence=\{spatialReady \? sequence : undefined\}/);
assert.match(review, /<M48EvidenceModeRail/);
assert.match(adjudication, /<M48EvidenceModeRail/);
assert.match(review, /<FieldFrame label="Выбор клипа"/);
assert.match(review, /label="Предыдущий клип"/);
assert.match(review, /label="Следующий клип"/);
assert.match(review, /<IconButton\s+label="Добавить объект"/);
assert.doesNotMatch(review, />\s*Добавить объект\s*<\/Button>/);
assert.ok(review.indexOf('label="Предыдущий клип"') < review.indexOf('<FieldFrame label="Выбор клипа"'));
assert.ok(review.indexOf('className="m48-review-workspace__clip-reviewed"') < review.indexOf('className="m48-review-workspace__topbar-end"'));
assert.doesNotMatch(review, /m48-review-workspace__topbar-center/);
assert.ok(review.indexOf('label="Добавить объект"') < review.indexOf('label="Закрыть проверку M4.8"'));
assert.doesNotMatch(review, /label="Сохранить всю проверку"/);
assert.match(review, /void save\(nextDrafts, "clip-status", \{ drafts, dirty \}\)/);
assert.match(review, /title: reason === "clip-status" \? "Статус клипа сохранён"/);
assert.match(review, /setDrafts\(rollback\.drafts\);\s*setDirty\(rollback\.dirty\);/);
assert.match(review, /className="m48-review-workspace__stage-shell"/);
assert.match(review, /className="m48-review-workspace__source-sticker"/);
assert.match(review, /materialRim=\{false\}/);
assert.match(review, /label="Добавить ещё один объект"/);
assert.match(review, /label="Сохранить изменения объекта"/);
assert.match(review, /<FieldFrame label="Проезд"/);
assert.match(review, /label="Объезд или запас"/);
assert.match(review, /<FieldFrame label="Непосредственная опасность"/);
assert.match(review, /label="Закрыть редактор объекта"/);
assert.doesNotMatch(review, /className="m48-review-workspace__(?:reviewbar|review-actions|source-state)"/);
assert.doesNotMatch(review, />\s*Сохранить изменения\s*<\/Button>/);
assert.doesNotMatch(review, /Выбранный объект|Рамка здесь|Начало здесь|Конец здесь|trimM48Tracklet|рамка переносится по клипу/);
assert.match(review, /sequence < selectedTracklet\.firstSequence \|\| sequence > selectedTracklet\.lastSequence/);
assert.match(player, /interactionMoved\([\s\S]*?boxInteraction\.startClient,[\s\S]*?event\.clientX/);
assert.match(player, /extentsDiffer\(boxInteraction\.originalExtent, extent\)/);
assert.match(player, /firstSequence: sequence/);
assert.match(player, /lastSequence: sequence/);
assert.doesNotMatch(review, /inspector=\{/);
assert.match(frame, /\{inspector \? <footer/);
assert.doesNotMatch(frameStyles, /border-(?:top|bottom):/);
assert.match(m48Styles, /\.m48-clip-player__pane-label\s*\{[^}]*border:\s*0/s);
assert.match(m48Styles, /\.m48-review-workspace__object-tools\s*\{[^}]*justify-content:\s*flex-start/s);
assert.match(m48Styles, /\.m48-review-workspace__topbar\s*\{[^}]*display:\s*grid[^}]*grid-template-columns:\s*minmax\(0, 1fr\) auto/s);
assert.match(m48Styles, /\.m48-evidence-mode-rail\s*\{[^}]*position:\s*absolute[^}]*left:\s*var\(--nodedc-space-4\)[^}]*translateY\(-50%\)/s);
assert.match(m48Styles, /\.m48-evidence-mode-controls\s*\{[^}]*flex-direction:\s*column[^}]*gap:\s*var\(--nodedc-space-2\)/s);
assert.match(m48Styles, /\.m48-evidence-mode-controls__text\s*\{[^}]*font-size:\s*var\(--nodedc-font-size-xs\)/s);
assert.match(m48Styles, /\.m48-review-workspace__clip-field\s*\{[^}]*19vw/s);
assert.match(m48Styles, /\.m48-review-workspace__clip-reviewed\s*\{[^}]*16vw/s);
assert.match(m48Styles, /\.m48-review-workspace__passage-field\s*\{[^}]*18vw/s);
assert.match(m48Styles, /\.m48-review-workspace__source-sticker\s*\{[^}]*position:\s*absolute[^}]*pointer-events:\s*none/s);
assert.match(
recordedStyles,
/\.laboratory-recorded-clip-player__timeline\.observation-timeline\s*\{[^}]*border:\s*0/s,
);
assert.match(
recordedStyles,
/grid-template-columns:\s*auto auto auto minmax\(12rem, 1fr\) auto/,
);
});
test("spatial legend exposes only layers present in the current evidence contract", () => {
const sourceOnly = laboratoryMetricLegendEntries({
pointCloudCount: 128,
localSurfaceCount: 0,
obstacles: [],
showCurrentIncrement: true,
showLocalSurface: false,
showRollingMap: false,
});
assert.deepEqual(sourceOnly, [{ id: "context", label: "Текущий кадр" }]);
const threat = laboratoryMetricLegendEntries({
pointCloudCount: 128,
localSurfaceCount: 64,
obstacles: [{
id: "obstacle-1",
decision: "threat",
state: "retained",
centroidBodyXyzM: [1, 0, 0],
cellCentersBodyXyzM: [[1, 0, 0]],
}],
showCurrentIncrement: true,
showLocalSurface: true,
showRollingMap: true,
});
assert.deepEqual(threat.map(({ id }) => id), [
"threat",
"context",
"local-surface",
"rolling",
]);
});
test("M4.8 full-screen workflow suspends the background evidence owner", () => {
const capability = readFileSync(
new URL("../src/workspaces/laboratory/annotation/useM48ReviewCapability.tsx", import.meta.url),
"utf8",
);
const archive = readFileSync(
new URL("../src/workspaces/laboratory/LaboratoryArchiveWorkspace.tsx", import.meta.url),
"utf8",
);
assert.match(capability, /\{ workspace: ReactNode; active: boolean \}/);
assert.match(archive, /!m48Review\.active \? <div className="laboratory-work-output">/);
assert.match(archive, /\{m48Review\.workspace\}/);
});
test("metric evidence keeps one WebGL renderer while frame labels advance", () => {
const source = readFileSync(
new URL("../src/components/laboratory/LaboratoryMetricEvidenceScene.tsx", import.meta.url),
"utf8",
);
assert.doesNotMatch(source, /renderer\.domElement\.setAttribute\("aria-label", label\)/);
assert.match(source, /renderer\.dispose\(\);[\s\S]{0,600}?\}, \[\]\);/);
assert.match(
source,
/querySelector\("canvas"\)[\s\S]*?setAttribute\("aria-label", label\)[\s\S]*?\}, \[label\]\);/,
);
});
@@ -0,0 +1,163 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let fetchM48SmallStaticRegression;
let fetchM48SmallStaticRegressionCases;
let fetchM48SmallStaticRegressionCase;
const resultId = `m48-small-static-passage-regression-${"a".repeat(64)}`;
const packId = `m48-object-quality-pack-${"b".repeat(64)}`;
const anchorId = `anchor-${"c".repeat(24)}`;
const authority = {
mode: "replay-simulated",
physical_live: false,
commands_enabled: false,
actuation_allowed: false,
navigation_or_safety_accepted: false,
};
before(async () => {
server = await createServer({ appType: "custom", logLevel: "silent", server: { middlewareMode: true } });
({
fetchM48SmallStaticRegression,
fetchM48SmallStaticRegressionCases,
fetchM48SmallStaticRegressionCase,
} = await server.ssrLoadModule("/src/core/laboratory/m48SmallStaticRegression.ts"));
});
after(async () => server?.close());
function response(value) {
return { ok: true, status: 200, json: async () => value };
}
test("M4.8R1 keeps pipeline identity and assisted evidence authority explicit", async () => {
const summary = await fetchM48SmallStaticRegression(resultId, {
fetcher: async () => response({
schema_version: "missioncore.m48-small-static-passage-regression-result-view/v1",
result_id: resultId,
pack_id: packId,
created_at_utc: "2026-08-24T12:00:00Z",
run_label: "M4.8R1",
pipeline_id: "m48-class-free-object-quality/v1",
experiment_id: "m48-small-static-passage-regression/v1",
accepted: false,
metrics: {
assisted_anchor_count: 14,
assisted_tracklet_count: 14,
anchor_clip_count: 8,
requires_avoidance_or_clearance_count: 12,
worker_recalled_anchor_count: 0,
worker_missed_anchor_count: 14,
assisted_anchor_recall: 0,
extent_iou_threshold: 0.5,
minimum_assisted_anchor_recall: 0.9,
},
gates: {
anchor_set_non_empty: true,
development_anchor_recall_target: false,
independent_truth_available: false,
},
decision: {
state: "failed-development-regression-baseline",
summary: "Worker 006 matched 0/14.",
next_action: "Publish another immutable run.",
},
ground_truth: false,
independent_truth: false,
authority,
}),
});
assert.equal(summary.pipelineId, "m48-class-free-object-quality/v1");
assert.equal(summary.experimentId, "m48-small-static-passage-regression/v1");
assert.equal(summary.metrics.workerMissedAnchorCount, 14);
assert.equal(summary.independentTruth, false);
});
test("M4.8R1 bounded case binds one exact assisted anchor and frozen objects", async () => {
const cases = await fetchM48SmallStaticRegressionCases(resultId, {
fetcher: async () => response({
schema_version: "missioncore.m48-small-static-passage-regression-case-catalog/v1",
result_id: resultId,
cases: [{
anchor_id: anchorId,
clip_id: "m48-clip-03",
sequence: 256,
requires_avoidance_or_clearance: true,
worker_candidate_count: 1,
best_iou: 0.01,
matched_at_threshold: false,
outcome: "missed-assisted-anchor",
}],
case_count: 1,
ground_truth: false,
authority,
}),
});
assert.equal(cases[0].outcome, "missed-assisted-anchor");
const item = await fetchM48SmallStaticRegressionCase(resultId, anchorId, {
fetcher: async () => response({
schema_version: "missioncore.m48-small-static-passage-regression-case-view/v1",
result_id: resultId,
pack_id: packId,
anchor: {
anchor_id: anchorId,
clip_id: "m48-clip-03",
object_id: "object-01",
sequence: 256,
extent_xyxy: [0.2, 0.3, 0.4, 0.7],
visibility: "visible",
geometry_association: "unknown",
freshness: "current",
motion: "static",
threat: "not-threat",
requires_avoidance_or_clearance: true,
},
comparison: {
anchor_id: anchorId,
clip_id: "m48-clip-03",
sequence: 256,
requires_avoidance_or_clearance: true,
worker_candidate_count: 1,
best_iou: 0.01,
matched_at_threshold: false,
outcome: "missed-assisted-anchor",
source_time_ns: 25_600_000_000,
anchor_extent_xyxy: [0.2, 0.3, 0.4, 0.7],
worker_objects: [{
prediction_id: "proposal-256-0",
extent_xyxy: [0.7, 0.2, 0.9, 0.6],
geometry_association: "associated",
freshness: "current",
motion: "static",
threat: "not-threat",
}],
best_prediction_id: "proposal-256-0",
extent_iou_threshold: 0.5,
},
camera_url: `/api/v1/laboratory/m48/packs/${packId}/source/clips/m48-clip-03/frames/256/camera`,
spatial_url: `/api/v1/laboratory/m48/packs/${packId}/source/clips/m48-clip-03/frames/256/spatial`,
ground_truth: false,
authority,
}),
});
assert.equal(item.anchor.sequence, 256);
assert.equal(item.comparison.workerObjects[0].predictionId, "proposal-256-0");
assert.equal(item.groundTruth, false);
});
test("M4.8R1 visual reuses the held viewer and keeps manual boxes exact-frame only", () => {
const visual = readFileSync(
new URL("../src/workspaces/laboratory/M48SmallStaticRegressionVisual.tsx", import.meta.url),
"utf8",
);
assert.match(visual, /<LaboratoryEvidenceViewer/);
assert.match(visual, /<M48BlindClipPlayer/);
assert.match(visual, /<M48EvidenceModeRail/);
assert.match(visual, /firstSequence: sequence,[\s\S]*lastSequence: sequence/);
assert.doesNotMatch(visual, /Rerun|MediaSource|setInterval/);
});
@@ -508,7 +508,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /LOCAL SLAM/);
assert.match(visual, /showLocalSurface/);
assert.match(visual, /pointCloudBodyXyzM=\{spatialFrame\.pointCloudBodyXyzM\}/);
assert.match(metricScene, /Local SLAM surface/);
assert.match(metricScene, /Локальная SLAM-поверхность/);
assert.match(visual, /showJumpToEnd=\{false\}/);
assert.doesNotMatch(visual, /Назад на 5 секунд/);
assert.doesNotMatch(visual, /Вперёд на 5 секунд/);
@@ -326,6 +326,47 @@ test("spatial timeline renders synchronized accumulation and playback controls",
assert.equal((markup.match(/type="range"/g) ?? []).length, 2);
});
test("recorded observation timeline uses compact icon transport and one inline rate trigger", () => {
const idleMarkup = renderToStaticMarkup(createElement(ObservationTimeline, {
active: true,
sourceCount: 2,
mode: "recorded",
seekable: true,
rangeNs: { min: 0, max: 20_000_000_000 },
currentNs: 5_000_000_000,
playing: false,
playbackRate: 1,
onSeek: () => undefined,
onPlayingChange: () => undefined,
onPlaybackRateChange: () => undefined,
}));
const playingMarkup = renderToStaticMarkup(createElement(ObservationTimeline, {
active: true,
sourceCount: 2,
mode: "recorded",
seekable: true,
rangeNs: { min: 0, max: 20_000_000_000 },
currentNs: 5_000_000_000,
playing: true,
playbackRate: 1,
onSeek: () => undefined,
onPlayingChange: () => undefined,
onPlaybackRateChange: () => undefined,
}));
assert.match(idleMarkup, /aria-label="Воспроизвести"/);
assert.match(idleMarkup, /lucide-play/);
assert.match(idleMarkup, /fill="currentColor"/);
assert.match(idleMarkup, /stroke-width="0"/);
assert.match(playingMarkup, /aria-label="Пауза"/);
assert.match(playingMarkup, /lucide-square/);
assert.match(playingMarkup, /fill="currentColor"/);
assert.match(idleMarkup, /nodedc-select-inline/);
assert.match(idleMarkup, />1×<\/span>/);
assert.doesNotMatch(idleMarkup, /nodedc-select-trigger__chevron/);
assert.doesNotMatch(idleMarkup, />Воспроизвести<|>Пауза</);
});
test("Rerun expands only root-relative session recordings onto the current origin", () => {
assert.equal(
resolveRerunSourceUrl(
@@ -11,6 +11,7 @@ let recordedMediaSeekableCoverage;
let recordedMediaFragmentUrl;
let recordedMediaDecodeStartSequence;
let recordedMediaSegmentAppendOrder;
let recordedMediaCanRollTarget;
before(async () => {
server = await createServer({
@@ -25,6 +26,7 @@ before(async () => {
recordedMediaFragmentUrl,
recordedMediaDecodeStartSequence,
recordedMediaSegmentAppendOrder,
recordedMediaCanRollTarget,
} = await server.ssrLoadModule("/src/components/RecordedFmp4Player.tsx"));
});
@@ -204,6 +206,14 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
);
});
test("recorded player preserves forward rolling playback but seeks backward clip loops", () => {
assert.equal(recordedMediaCanRollTarget(20, 21, true, true), true);
assert.equal(recordedMediaCanRollTarget(20, 20, true, true), true);
assert.equal(recordedMediaCanRollTarget(20, 1, true, true), false);
assert.equal(recordedMediaCanRollTarget(20, 21, false, true), false);
assert.equal(recordedMediaCanRollTarget(20, 21, true, false), false);
});
test("loading and error overlays fully conceal recorded camera pixels", async () => {
const css = await readFile(
new URL("../src/styles/observation.css", import.meta.url),
@@ -0,0 +1,20 @@
{
"schema_version": "missioncore.laboratory-evidence-definition/v2",
"work_id": "m48-object-centric-quality",
"evidence_lifecycle": [
{
"phase": "review",
"runtime_relative_root": "m48/object-quality-packs",
"result_id_prefix": "m48-object-quality-pack",
"document_name": "manifest.json",
"schema_version": "missioncore.m48-object-centric-quality-pack/v1"
},
{
"phase": "result",
"runtime_relative_root": "m48/object-quality-results",
"result_id_prefix": "m48-object-quality-result",
"document_name": "manifest.json",
"schema_version": "missioncore.m48-object-centric-quality-result/v1"
}
]
}
@@ -0,0 +1,10 @@
{
"schema_version": "missioncore.laboratory-evidence-definition/v1",
"work_id": "m48-small-static-passage-regression",
"evidence": {
"runtime_relative_root": "m48/small-static-passage-regression-results",
"result_id_prefix": "m48-small-static-passage-regression",
"document_name": "manifest.json",
"schema_version": "missioncore.m48-small-static-passage-regression-result/v1"
}
}
+28
View File
@@ -1,6 +1,34 @@
{
"schema_version": "missioncore.laboratory-execution-registry/v1",
"definitions": [
{
"work_id": "m48-small-static-passage-regression",
"lifecycle": "canonical",
"isolation": "core-adapter",
"adapter_id": "canonical.m48-small-static-passage-regression/v1",
"input_roles": ["pack_root", "correction_session_path", "profile_path"],
"contracts": {
"source": "missioncore.m48-object-centric-quality-pack/v1",
"provider": "missioncore.m48-assisted-object-correction-session/v1",
"graph": "missioncore.m48-assisted-anchor-comparison/v1",
"run": "missioncore.laboratory-run/v1",
"evidence": "missioncore.m48-small-static-passage-regression-result/v1"
}
},
{
"work_id": "m48-object-centric-quality",
"lifecycle": "canonical",
"isolation": "core-adapter",
"adapter_id": "canonical.m48-object-centric-quality/v1",
"input_roles": ["pack_root", "truth_seal_root"],
"contracts": {
"source": "missioncore.m48-object-centric-quality-pack/v1",
"provider": "missioncore.m48-object-truth-seal/v1",
"graph": "missioncore.m48-object-centric-quality-graph/v1",
"run": "missioncore.laboratory-run/v1",
"evidence": "missioncore.m48-object-centric-quality-result/v1"
}
},
{
"work_id": "m4-replay-threat",
"lifecycle": "canonical",
@@ -0,0 +1,235 @@
{
"schema_version": "missioncore.m48-object-quality-selection/v1",
"selection_id": "m48-ravnoves00-balanced-connected-clips/v1",
"source_id": "RAVNOVES00",
"source_session_id": "20260720T065719Z_viewer_live",
"selection_basis": "prediction-frozen-source-curation-before-independent-truth",
"camera_frame_size": {
"width": 800,
"height": 600
},
"selection_hypothesis_profile": {
"derivation": "exact-frozen-prediction-rows-before-independent-truth",
"small_obstacle_max_normalized_area": 0.001,
"fisheye_edge_margin_normalized": 0.08,
"sparse_scene_max_median_prediction_count": 2.0
},
"clips": [
{
"clip_id": "m48-clip-01",
"component_id": "m48-component-development-01",
"route_block": "route-block-01",
"time_block": "time-block-01",
"split": "development",
"start_sequence": 1,
"end_sequence": 61
},
{
"clip_id": "m48-clip-02",
"component_id": "m48-component-development-01",
"route_block": "route-block-01",
"time_block": "time-block-01",
"split": "development",
"start_sequence": 121,
"end_sequence": 181
},
{
"clip_id": "m48-clip-03",
"component_id": "m48-component-development-01",
"route_block": "route-block-01",
"time_block": "time-block-01",
"split": "development",
"start_sequence": 241,
"end_sequence": 301
},
{
"clip_id": "m48-clip-04",
"component_id": "m48-component-development-02",
"route_block": "route-block-01",
"time_block": "time-block-02",
"split": "development",
"start_sequence": 421,
"end_sequence": 481
},
{
"clip_id": "m48-clip-05",
"component_id": "m48-component-development-02",
"route_block": "route-block-01",
"time_block": "time-block-02",
"split": "development",
"start_sequence": 581,
"end_sequence": 641
},
{
"clip_id": "m48-clip-06",
"component_id": "m48-component-development-02",
"route_block": "route-block-02",
"time_block": "time-block-02",
"split": "development",
"start_sequence": 821,
"end_sequence": 881
},
{
"clip_id": "m48-clip-07",
"component_id": "m48-component-development-03",
"route_block": "route-block-02",
"time_block": "time-block-03",
"split": "development",
"start_sequence": 1041,
"end_sequence": 1101
},
{
"clip_id": "m48-clip-08",
"component_id": "m48-component-development-03",
"route_block": "route-block-02",
"time_block": "time-block-03",
"split": "development",
"start_sequence": 1221,
"end_sequence": 1281
},
{
"clip_id": "m48-clip-09",
"component_id": "m48-component-development-03",
"route_block": "route-block-02",
"time_block": "time-block-03",
"split": "development",
"start_sequence": 1421,
"end_sequence": 1481
},
{
"clip_id": "m48-clip-10",
"component_id": "m48-component-development-04",
"route_block": "route-block-03-development",
"time_block": "time-block-04",
"split": "development",
"start_sequence": 1681,
"end_sequence": 1741
},
{
"clip_id": "m48-clip-11",
"component_id": "m48-component-development-04",
"route_block": "route-block-03-development",
"time_block": "time-block-04",
"split": "development",
"start_sequence": 1830,
"end_sequence": 1890
},
{
"clip_id": "m48-clip-12",
"component_id": "m48-component-development-04",
"route_block": "route-block-03-development",
"time_block": "time-block-04",
"split": "development",
"start_sequence": 2041,
"end_sequence": 2101
},
{
"clip_id": "m48-clip-13",
"component_id": "m48-component-validation-01",
"route_block": "route-block-03-validation",
"time_block": "time-block-05",
"split": "validation",
"start_sequence": 2191,
"end_sequence": 2251
},
{
"clip_id": "m48-clip-14",
"component_id": "m48-component-validation-01",
"route_block": "route-block-03-validation",
"time_block": "time-block-05",
"split": "validation",
"start_sequence": 2371,
"end_sequence": 2431
},
{
"clip_id": "m48-clip-15",
"component_id": "m48-component-validation-01",
"route_block": "route-block-03-validation",
"time_block": "time-block-05",
"split": "validation",
"start_sequence": 2551,
"end_sequence": 2611
},
{
"clip_id": "m48-clip-16",
"component_id": "m48-component-validation-02",
"route_block": "route-block-04",
"time_block": "time-block-06",
"split": "validation",
"start_sequence": 2731,
"end_sequence": 2791
},
{
"clip_id": "m48-clip-17",
"component_id": "m48-component-validation-02",
"route_block": "route-block-04",
"time_block": "time-block-06",
"split": "validation",
"start_sequence": 2911,
"end_sequence": 2971
},
{
"clip_id": "m48-clip-18",
"component_id": "m48-component-validation-02",
"route_block": "route-block-04",
"time_block": "time-block-06",
"split": "validation",
"start_sequence": 3111,
"end_sequence": 3171
},
{
"clip_id": "m48-clip-19",
"component_id": "m48-component-validation-03",
"route_block": "route-block-04",
"time_block": "time-block-07",
"split": "validation",
"start_sequence": 3291,
"end_sequence": 3351
},
{
"clip_id": "m48-clip-20",
"component_id": "m48-component-validation-03",
"route_block": "route-block-04",
"time_block": "time-block-07",
"split": "validation",
"start_sequence": 3471,
"end_sequence": 3531
},
{
"clip_id": "m48-clip-21",
"component_id": "m48-component-validation-03",
"route_block": "route-block-05",
"time_block": "time-block-07",
"split": "validation",
"start_sequence": 3651,
"end_sequence": 3711
},
{
"clip_id": "m48-clip-22",
"component_id": "m48-component-validation-04",
"route_block": "route-block-05",
"time_block": "time-block-08",
"split": "validation",
"start_sequence": 3831,
"end_sequence": 3891
},
{
"clip_id": "m48-clip-23",
"component_id": "m48-component-validation-04",
"route_block": "route-block-05",
"time_block": "time-block-08",
"split": "validation",
"start_sequence": 4051,
"end_sequence": 4111
},
{
"clip_id": "m48-clip-24",
"component_id": "m48-component-validation-04",
"route_block": "route-block-05",
"time_block": "time-block-08",
"split": "validation",
"start_sequence": 4429,
"end_sequence": 4489
}
]
}
@@ -0,0 +1,72 @@
{
"schema_version": "missioncore.m48-object-quality-profile/v1",
"profile_id": "m48-ravnoves00-object-quality/v1",
"source_graph_id": "reference-perception-graph/v2",
"source_profile_id": "m4-ravnoves00-recorded-realtime/v1",
"clip_contract": {
"minimum_clip_count": 20,
"maximum_clip_count": 30,
"minimum_duration_seconds": 5.0,
"maximum_duration_seconds": 10.0,
"required_validation_hypotheses": [
"prediction-associated",
"prediction-fisheye-edge",
"prediction-moving",
"prediction-small-obstacle",
"prediction-sparse-scene",
"prediction-static",
"prediction-threat",
"prediction-unassociated"
],
"selection_hypothesis_profile": {
"derivation": "exact-frozen-prediction-rows-before-independent-truth",
"small_obstacle_max_normalized_area": 0.001,
"fisheye_edge_margin_normalized": 0.08,
"sparse_scene_max_median_prediction_count": 2.0
},
"splits": [
"development",
"validation"
],
"connected_component_split_overlap_allowed": false,
"route_or_time_block_split_overlap_allowed": false,
"release_gate_split": "validation"
},
"review_contract": {
"review_unit": "clip-local-object-tracklet",
"extent_labels": "sparse-normalized-xyxy-keyframes",
"state_labels": "contiguous-tracklet-state-segments",
"per_frame_expansion": {
"extent": "linear-between-bounding-keyframes",
"visibility": "left-keyframe-hold",
"state": "contiguous-state-segment"
},
"semantic_class_labels_allowed": false,
"independent_reviewers_required": 2,
"adjudication_required": true,
"predictions_frozen_before_label_reveal": true,
"prediction_content_visible_to_reviewers": false,
"selection_hypotheses_visible_to_reviewers": false
},
"matching": {
"extent_iou_threshold": 0.5
},
"release_thresholds": {
"terminal_outcome_accounting": 1.0,
"false_free_space_claims": 0,
"obstacle_presence_precision": 0.9,
"obstacle_presence_recall": 0.9,
"critical_corridor_obstacle_recall": 0.95,
"geometry_association_correctness": 0.9,
"freshness_correctness": 0.9,
"motion_decision_correctness": 0.9,
"critical_threat_not_threat": 0
},
"authority": {
"mode": "replay-simulated",
"physical_live": false,
"commands_enabled": false,
"actuation_allowed": false,
"navigation_or_safety_accepted": false
}
}
@@ -0,0 +1,13 @@
{
"schema_version": "missioncore.m48-small-static-passage-regression-profile/v1",
"profile_id": "m48-small-static-passage-regression/v1",
"pipeline_id": "m48-class-free-object-quality/v1",
"experiment_id": "m48-small-static-passage-regression/v1",
"human_lab_id": "M4.8",
"run_label": "M4.8R1",
"anchor_selection": "operator-added-tracklets-in-reviewed-clips/v1",
"extent_iou_threshold": 0.5,
"minimum_assisted_anchor_recall": 0.9,
"minimum_anchor_count": 1,
"independent_truth": false
}
+23
View File
@@ -185,6 +185,29 @@ The evidence slot has two admitted renderers:
- `diagnostic-model` — a specialized visual result such as the LAB E28 L2.6
surface/timeline/review viewer.
Recorded camera clips used for review are a frozen sub-contract of the admitted
viewer, `missioncore.laboratory-recorded-clip-viewer/v1`, implemented by
`LaboratoryRecordedClipPlayer`. It owns the generation-bound fMP4 manifest,
bounded MediaSource buffering, source-time playback clock, clip looping and the
canonical timeline. A LAB may provide typed overlays and an alternative spatial
scene, but it may not implement its own frame timer, per-frame JPEG playback,
media cache, port, Rerun blueprint or loading grammar. Exact JPEG remains a
paused-frame/fallback evidence endpoint, never the continuous playback transport.
Forward frame progression may roll an already-buffered segment target; a
backward seek or clip loop must perform an explicit decoder seek and remain
decoder-ready without exposing a per-frame loader.
When recorded camera and frame-indexed spatial evidence are both required for
one review question, the shared player presents them simultaneously on the same
media clock. The camera remains the clock owner; a bounded experiment-neutral
spatial cache prefetches exact source sequences, including across the loop
boundary. Mode switching changes presentation only. It must not pause the clock,
hide the companion camera, create another transport, or flash a full-stage
loader between spatial frames. Connected M4.8 workflows project CAMERA, 3D and
PLAN through the same left-side vertical glass rail of canonical circular
actions; the report, assisted correction and formal review flows do not invent
separate mode-control geometry.
An admitted diagnostic viewer may own the result interaction internally when
the evidence itself is the review/result instrument, as in E28 and E30. This is
not permission to omit the result from a new ordinary LAB report. New bounded
@@ -173,6 +173,37 @@ The viewer frame must:
- remain keyboard-addressable and restore the previous surface on Escape;
- avoid hard-coded product colors and application-local focus/hover states.
Connected recorded review uses stacked viewer chrome: source/case navigation
occupies a dedicated header above the visual viewport; playback controls and
the timeline occupy a dedicated transport panel below it. CAMERA/3D/PLAN use
one reusable vertical `GlassSurface` rail on the left of the viewport. Its
three actions are canonical 46 px circular `IconButton` controls: an outline
camera glyph, the `3D` text glyph and an outline plan glyph. The rail may float
over the viewport but must not resize it; every other control region remains
outside the camera/spatial interaction area. These regions form one seamless
viewer surface without per-region outlines or gaps. When spatial
and camera evidence are shown together, the shared vertical divider is
pointer- and keyboard-resizable, preserves one mounted camera transport, and
may only reveal its visual affordance while hovered, focused, or dragged.
CAMERA visibility remains independent. 3D/PLAN remain mutually exclusive
spatial modes whose selected mode can be disabled while CAMERA stays visible.
The UI must preserve at least one visible evidence channel and must not encode
CAMERA, 3D and PLAN as one exclusive group.
For recorded clip review, continuous camera playback must use the shared
`missioncore.laboratory-recorded-clip-viewer/v1` transport: one immutable
manifest admission followed by bounded generation-bound fMP4 fragments on the
source media clock. A visible loader on every source frame, a LAB-owned timer,
or a LAB-owned video/Rerun configuration is a contract regression. Spatial modes
whose evidence cannot be delivered at source pace remain explicit frame-step
modes; they must not stall or relabel the camera clock as realtime. When spatial
evidence is admitted at source pace, it follows the same source sequence through
a bounded look-ahead cache. CAMERA + 3D/PLAN remain simultaneously visible, and
no intermediate spatial miss may replace or stop the camera surface.
The shared player may retain a rolling target only for the same or a later
segment. Rewind and clip-loop transitions must seek backward explicitly while
keeping the admitted generation and decoder owner mounted.
### 3D and 2D policy
Choose the default representation from the operator question:
@@ -187,6 +218,10 @@ When both questions matter, expose 2D and 3D as modes of the same viewer. They
must use the same selected case and immutable source indices. Do not create a
second LAB page or duplicate the evidence state.
For M4.8, **3D/PLAN always includes the synchronized RIGHT-camera companion**.
The point cloud must advance with the recorded media clock; a paused 3D snapshot
with disabled playback is not admissible evidence for connected-object review.
For E30, **camera + projected LiDAR is the default** because the first review
question is whether a camera claim, its bbox and the projected points refer to
the same visible object. A black pixel-plane scatter without the exact camera
@@ -175,6 +175,36 @@ receives a concise typed projection.
DOM or CSS classes. The product UI test discovers every `ENNResult.tsx`
automatically and rejects such a fork.
`components/laboratory/LaboratoryRecordedClipPlayer.tsx` exclusively owns the
versioned `missioncore.laboratory-recorded-clip-viewer/v1` camera transport and
clock. Feature renderers may add typed overlays or a synchronized spatial scene
through its slots. Source-paced spatial evidence follows exact media sequences
through a bounded reusable look-ahead cache; frame-step remains an explicit
capability only when the source cannot keep pace. Experiment-named players must not fetch a JPEG
per playback frame, instantiate MediaSource, schedule frame timers, or declare a
new Rerun receiver/blueprint. Forward buffered progression and backward
loop/seek are separate shared-player transitions; a backward target cannot be
treated as an ordinary rolling-buffer advance.
The shared player owns the admitted `primary` camera and `companion` camera +
spatial presentations. A feature mode may not disable playback, unmount the
camera, or create a second viewer configuration merely to show 3D/PLAN.
`LaboratoryEvidenceViewer` owns the reusable `stacked` chrome layout: one header,
one visual stage and one transport/timeline rail joined into a seamless surface.
`LaboratoryRecordedClipPlayer` reuses the canonical `SplitPane` for resizable
spatial + companion-camera evidence while keeping the recorded camera owner
mounted across CAMERA/3D/PLAN transitions. Feature code supplies typed actions
and modes; it does not declare a feature-local splitter. M4.8 reuses one
`M48EvidenceModeRail` projection in the report, candidate-assisted correction,
independent review and adjudication surfaces. The rail is the admitted
viewport overlay: a canonical `GlassSurface` containing three default-size
`IconButton` controls and no local control geometry. CAMERA/3D/PLAN therefore
do not return to the stacked header or fork per workflow:
CAMERA visibility is independent from the nullable 3D/PLAN spatial mode, and
their state transition cannot hide both channels. Candidate seed data belongs
to the correction contract only; it cannot enter the blind source decoder or
upgrade assisted evidence to independent truth.
Legacy/integrated diagnostic viewers may keep a result interaction inside the
evidence slot only where that viewer is already the admitted result instrument.
This exception does not apply automatically to a new LAB.
@@ -2,9 +2,10 @@
Date: 2026-08-05
Status: in progress; M4.0–M4.6 accepted, the M4.7 canonical graph and Worker
006 shadow artifact are implemented locally, and Worker preflight/full shadow
acceptance plus durable cutover remain open
Status: in progress; M4.0–M4.6 and the M4.7 canonical lossless Worker 006
shadow are accepted, M4.8 prediction-frozen review-pack preparation is complete,
and the independent reviews/adjudication, M4.8 scoring, M4.9 release candidate
and durable Worker cutover remain open
Audit base: `1b3e0b3` on `feat/simulation-polygon-s1`
@@ -500,10 +501,11 @@ Physical mounted threat acceptance remains outside Milestone 4.
### M4.7 — cut Worker 006 over to the canonical graph
Status: implementation complete locally on 2026-08-23; Worker 006 preflight,
full lossless shadow evidence and durable process replacement are not yet
accepted. The current E15 worker and Triton identities remain the rollback
predecessor. No K1, Zarya or connection-stack change is part of this phase.
Status: the isolated Worker 006 preflight and full lossless shadow were accepted
on 2026-08-23 with all `4,489` frames delivered, zero parity mismatches and queue
high-water mark `2`. Durable process replacement is not implemented or accepted.
The current E15 worker and Triton identities remain the rollback predecessor. No
K1, Zarya or connection-stack change is part of this phase.
Deliverables:
@@ -533,8 +535,9 @@ adds a separate object-centric review contract; it does not require class labels
Dataset construction:
- freeze 20–30 connected clips of 5–10 seconds across route/time blocks;
- include occupied object, background false-positive, partial occlusion, fisheye
edge, small obstacle, moving crossing, static obstacle and no-object strata;
- derive the private balancing hypotheses only from already frozen prediction
rows (associated/unassociated, moving/static/threat, small, fisheye-edge and
sparse-scene signals); never use unrevealed truth to select the release set;
- label obstacle presence/extent, current geometry association, freshness,
moving/static/unknown and virtual-corridor threat/unknown;
- freeze graph predictions before review labels are joined;
@@ -1194,12 +1197,199 @@ accepting the run. This makes Worker 006 usable without touching the stabilized
K1/Zarya connection path.
Local contract, graph, result-sealing, artifact and historical-rollback tests
pass. This is implementation evidence only. It does not claim that Worker 006
has the pinned local-surface input, that preflight has passed, that the 4,489
frame graph shadow matches the accepted M4.5R/M4.6 ledgers, or that the durable
E15 command has been replaced. Those are the next M4.7 acceptance actions, in
that order. K1, Zarya, the stable connection path and physical-live authority
remain untouched.
pass. Worker result
`m47-reference-graph-5f6a851cd655c7cf07c3025dacadbc188018b0afa97eda3a08802266e12da87d`
sealed the full shadow with `4,489/4,489` admitted and delivered frames, zero
failed/stale/superseded/rejected/unavailable outcomes and zero mismatches in all
seven parity dimensions. LAB result
`m47-reference-graph-lab-49678f0a7c628c7e991af0964fa57d005baa027d2d1eea19f38bbfe27ed39ce5`
therefore opens the independent object-centric quality gate. This does not claim
that the durable E15 command has been replaced: the accepted runner is explicitly
one-shot shadow-only, and the required persistent service, telemetry-continuity
and automatic rollback contract do not yet exist. K1, Zarya, the stable
connection path and physical-live authority remain untouched.
### 2026-08-24 — M4.8 independent review pack frozen
The first provenance-complete M4.8 pack is
`m48-object-quality-pack-680c091cd81cce802931dbb8987db6f26dc568c5d7395166cda2e9e5a4c78e27`.
It binds the accepted M4.7 LAB manifest, exact M4.6 threat ledger and exact
single-sample camera-fragment hashes for all `4,489` source frames, plus the
adapter, selection, camera index, graph, threat and geometry manifests/ledgers by
SHA-256. The selected review surface contains `24` non-overlapping connected
clips of approximately six seconds each (`12` development and `12` validation;
`1,464` selected frames) across six split-local route blocks and eight
split-local time blocks. All eight private balancing hypotheses are derived
mechanically from the frozen prediction rows and are present in validation; the
reviewer projection contains neither those hypotheses nor frozen boxes, IDs,
scores, model identity or semantic-class tasks.
The pack state is intentionally
`prepared-predictions-frozen-labels-unavailable`. It is not an accepted quality
result. Two distinct capability-bound reviewers must complete the class-free
tracklet review, both submissions must freeze, and a separate adjudication must
seal before the frozen predictions can be joined. Only then may the deterministic
per-frame ledger, critical-first failure atlas and M4.8 thresholds produce an
ACCEPT or REJECT. Release gates use validation only; development and combined
metrics remain diagnostic. No aggregate metric may waive a false-free claim, critical
miss, hidden terminal outcome or critical `threat`→`not-threat` error.
### 2026-08-24 — M4.8 laboratory architecture hardening
M4.8 is represented by one catalog work,
`m48-object-centric-quality`, whose evidence lifecycle advances from the
prepared review pack to the terminal quality result instead of publishing two
competing LAB cards. Review and adjudication use one shared focus-owning
laboratory workspace frame, while opening either full-screen capability unmounts
the background work output. Consequently only one camera decoder, spatial
renderer and playback clock can own the selected evidence at a time.
The M4.8 surface reuses the shared recorded camera and metric spatial viewers;
it defines no Rerun blueprint, receiver, window identifier or per-LAB viewer
configuration. Spatial legends are derived from admitted data and enabled
layers, so source-only review does not advertise unavailable threat, rolling or
local-surface semantics. Recorded-camera decode lanes and source manifests are
bounded by independent LRU caches. These changes do not alter the stabilized K1
connection, control or physical-live path.
The earlier M4.8 camera surface nevertheless still advanced playback through an
experiment-local timer and decoded one HTTP JPEG per frame. On the 10 FPS source,
each request also reopened and validated the full neutral pack, producing
approximately 0.17–0.42 s frame latency and a visible loader between frames.
That path is removed from continuous playback. M4.8 now consumes the frozen
`missioncore.laboratory-recorded-clip-viewer/v1`: the backend validates the
content-addressed pack and all selected camera fragment hashes once, binds the
canonical recorded-media generation, and the browser admits one compact
manifest before fetching bounded fMP4 fragments ahead of the source clock.
Switching clips, review/adjudication surfaces, or CAMERA/3D/PLAN modes does not
create another media configuration or another port. The exact JPEG endpoint is
retained only for a paused-frame/failure fallback and no longer participates in
normal playback.
The camera-fragment identity remains byte-exact. The replay graph stores integer
nanoseconds while the fMP4 boundary is represented through its media timescale;
the binding therefore admits at most `1,000 ns` of representation drift and
rejects any larger timeline change. Live acceptance on the canonical backend
confirmed a warm source projection in `12–16 ms`, the manifest in `13 ms`, and
21 init/fragment reads at `6.13 ms` mean, `10.94 ms` p95 and `23.02 ms` maximum.
The first post-process durable-package restoration is one explicit initial
admission and is not repeated per frame.
Acceptance also exposed and closed a shared loop defect: a backward clip target
had been misclassified as forward rolling-buffer progress. The shared player now
keeps rolling only for the same or a later segment and performs a decoder seek
for rewind/loop. A live six-second clip crossed `5.832 → 6.062 → 0.177 s` with
`readyState=4`, continuous playing state and no frame loader.
This viewer version is held as an architecture invariant. New LABs reuse
`LaboratoryRecordedClipPlayer`; they may supply domain overlays and a typed
alternative scene, but cannot own frame timers, MediaSource, per-frame playback
fetches, Rerun receivers or loading grammar. CAMERA is continuous and driven by
the recorded media clock. M4.8 `3D` and `PLAN` use the shared companion
presentation: the spatial scene and RIGHT camera remain visible together, play
from one media clock, and switching representation does not pause or remount the
decoder. Exact spatial frames are prefetched by a bounded 14-frame look-ahead
window, retained in a 24-frame client cache, and served through a 256-frame
backend LRU; the cache also looks through the clip-loop boundary.
Live acceptance on `m48-clip-02` confirmed camera progression
`11.991 → 14.841 s` and spatial progression `121 → 149` over the same interval.
Across 12 consecutive observations the 3D sequence changed 12 times with zero
loader observations. A separate loop check crossed `17.904 → 12.010 s`, advanced
16 distinct spatial sequences and again exposed no intermediate loader. This is
recorded source-paced evidence, not physical-live or navigation authority.
The selected evidence is not uniformly body-frame qualified: `1,202/1,464`
review frames expose the bounded current LiDAR increment, while `262` are
camera-only because no qualified body frame exists. The first selected clip is
camera-only for all `61` frames; the remaining clips have partial spatial
coverage. The review UI presents those frames as explicit unavailable evidence,
uses one admitted channel, creates no empty spatial canvas and never interprets
the absence as free space.
The frozen evaluation now runs through the canonical laboratory runner and
emits a content-addressed run receipt alongside the quality result. This is
execution provenance, not gate acceptance: the current pack remains at `0/2`
frozen independent reviews, with no adjudication, truth seal or evaluated M4.8
result.
### 2026-08-24 — M4.8 Worker 006 assisted correction LAB
The operator-facing final check no longer starts from an empty annotation
surface. A separate capability-bound correction session projects the immutable
Worker 006 prediction rows into editable class-free boxes before the first clip
opens. The current pack contributes `5,236` boxes across all `1,464` frames and
all `24` clips. The operator selects, moves, resizes or deletes an existing box,
draws a missing box, and then explicitly marks each clip reviewed.
This workflow reuses the same `LaboratoryRecordedClipPlayer`, camera/3D/PLAN
controls, split pane, timeline and spatial cache as the blind workflow. It adds
no viewer, Rerun blueprint, port, media transport or experiment-local playback
clock. Because the accepted M4.7 output has no provider tracklet identity, each
frozen detection is projected honestly as a one-frame editable object; the LAB
does not fabricate temporal identity between adjacent detections.
The correction artifact stores the frozen prediction-row SHA-256, original
Worker identity, complete corrected clip set and a deterministic human delta:
confirmed candidates, unchanged candidates, modified candidates, deleted false
positives and added misses. Its assistance mode is
`frozen-candidate-seeded`; candidate predictions are explicitly visible, model
scores and semantic classes remain absent, and `independent_truth_eligible` is
always false. Freezing this artifact therefore supplies regression evidence for
Worker 006 without pretending that candidate-assisted review satisfies the
two-reviewer independent release gate above.
The correction UI persists the clip-level `reviewed` transition immediately;
there is no second global Save action after the operator changes that checker.
Object geometry/state edits retain their bounded explicit Save action. Success
and error statuses use the canonical independently timed toast lifecycle and
roll a failed clip-status transition back to the last server revision while
preserving any pre-existing dirty object edits.
Small static objects are not split into semantic one-off rules for bins,
bollards, pipes or road hemispheres. M4.8 already records two orthogonal state
dimensions: `threat` is the immediate threat decision, while
`critical_corridor_obstacle` means that the object constrains passage and must
receive avoidance/clearance treatment even when `threat=not-threat`. The LAB UI
therefore exposes the latter as the separate **Проезд → Объезд или запас**
option. A camera rectangle is review evidence only: it cannot become
an oversized 3D collider. Future planner clearance must derive from admitted
LiDAR/local-occupancy geometry, its uncertainty envelope and the configured
vehicle footprint. This distinction is part of the durable M4.8 contract and
must not appear or disappear with detector-class tuning.
### 2026-08-24 — M4.8R1 small-static passage regression baseline
The first correction-derived regression is a new experiment inside the existing
M4.8 human LAB, not a replacement for the assisted-correction run. It keeps the
`m48-class-free-object-quality/v1` pipeline fixed, uses experiment
`m48-small-static-passage-regression/v1`, and publishes append-only run label
`M4.8R1`. The source correction session and frozen Worker 006 pack are read-only
inputs; no correction revision, pack artifact or earlier LAB result is mutated.
Canonical run
`m48-small-static-passage-regression-3e3a2001f87fd3adcb736de52a65e42515044d83faa3515f892705b40915c084`
snapshots revision 29 of the assisted correction. It contains `14`
operator-added exact-frame anchors across `8` reviewed clips, including `12`
anchors marked **Объезд или запас**. Against the frozen `5,236` Worker 006 boxes,
the exact-frame class-free comparator found `0/14` matches at IoU `>= 0.50`.
This deliberately fails the diagnostic `0.90` assisted-anchor recall target and
establishes a concrete miss baseline for the next perception experiment.
The result is not independent truth: the operator saw the Worker 006 candidates,
and the selection is intentionally biased toward objects that required manual
addition. It therefore cannot produce unbiased precision/recall or satisfy the
two-reviewer M4.8 release gate. A camera rectangle is also not a metric collider;
clearance and passability remain functions of admitted LiDAR/local occupancy,
uncertainty and the configured vehicle footprint. Physical live, navigation,
commands, actuation and collision-safety authority remain false.
The regression reuses the held M4.8 recorded viewer and its CAMERA/3D/PLAN,
single media clock, timeline and bounded spatial cache. Each assisted anchor and
the frozen Worker objects are shown only on their exact source frame; the UI does
not drift a manually drawn rectangle across subsequent frames or fabricate a
track. A later Worker candidate must publish another immutable M4.8R run against
the frozen seed, leaving this baseline available for before/after comparison.
## Implementation order
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Freeze the source-scoped RAVNOVES00 M4.8 independent-review pack."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from k1link.laboratory.m48_ravnoves00_pack import prepare_m48_ravnoves00_pack
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--m47-lab-root", type=Path, required=True)
parser.add_argument("--graph-result-root", type=Path, required=True)
parser.add_argument("--threat-result-root", type=Path, required=True)
parser.add_argument("--geometry-result-root", type=Path, required=True)
parser.add_argument("--camera-index", type=Path, required=True)
parser.add_argument("--selection", type=Path, required=True)
parser.add_argument("--frozen-at-utc", required=True)
parser.add_argument("--output-root", type=Path, required=True)
args = parser.parse_args()
result = prepare_m48_ravnoves00_pack(
m47_lab_root=args.m47_lab_root,
graph_result_root=args.graph_result_root,
threat_result_root=args.threat_result_root,
geometry_result_root=args.geometry_result_root,
camera_index_path=args.camera_index,
selection_path=args.selection,
frozen_at_utc=args.frozen_at_utc,
output_root=args.output_root,
)
print(
json.dumps(
{
"result_id": result.result_id,
"result_root": str(result.result_root),
"status": result.report["status"],
"clip_count": result.report["metrics"]["clip_count"],
"frame_count": result.report["metrics"]["frame_count"],
"truth_labels_available": False,
},
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Publish one canonical append-only M4.8 small-static regression run."""
from __future__ import annotations
import argparse
import json
import socket
from pathlib import Path
from k1link.compute.pipeline_telemetry import JsonlPipelineTelemetrySink
from k1link.laboratory import (
LaboratoryEvidenceRegistry,
LaboratoryExecutionRegistry,
LaboratoryRunner,
LaboratoryRunRequest,
)
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument("--pack-root", type=Path, required=True)
parser.add_argument("--correction-session", type=Path, required=True)
parser.add_argument("--profile", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
parser.add_argument("--receipt-root", type=Path, required=True)
parser.add_argument("--telemetry-path", type=Path, required=True)
parser.add_argument("--run-id", required=True)
parser.add_argument("--request-id", required=True)
return parser
def main() -> int:
args = _parser().parse_args()
repository_root = Path(__file__).resolve().parents[1]
evidence = LaboratoryEvidenceRegistry.from_directory(
repository_root / "config" / "laboratories"
)
execution = LaboratoryExecutionRegistry.from_file(
repository_root / "config" / "laboratory-execution.json",
evidence,
)
runner = LaboratoryRunner(
registry=execution,
evidence_registry=evidence,
sink=JsonlPipelineTelemetrySink(args.telemetry_path),
)
pack_id = args.pack_root.name
result = runner.run(
LaboratoryRunRequest(
work_id="m48-small-static-passage-regression",
run_id=args.run_id,
request_id=args.request_id,
contour_id="mission-core-laboratory",
agent_id="local-control-plane",
node_id=socket.gethostname(),
source_id="RAVNOVES00",
source_package_id=pack_id,
method_id="m48-small-static-passage-regression/v1",
inputs={
"pack_root": args.pack_root,
"correction_session_path": args.correction_session,
"profile_path": args.profile,
},
output_root=args.output_root,
receipt_root=args.receipt_root,
)
)
print(json.dumps({
"result_id": result.result_id,
"result_root": str(result.result_root),
"receipt_id": result.receipt_id,
"receipt_root": str(result.receipt_root),
}, ensure_ascii=False, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+4
View File
@@ -2,8 +2,10 @@
from k1link.laboratory.evidence_registry import (
LABORATORY_EVIDENCE_DEFINITION_SCHEMA,
LABORATORY_EVIDENCE_LIFECYCLE_DEFINITION_SCHEMA,
LaboratoryEvidenceDefinition,
LaboratoryEvidenceRegistry,
LaboratoryEvidenceVariant,
LaboratoryRegistryError,
)
from k1link.laboratory.evidence_report import (
@@ -34,9 +36,11 @@ from k1link.laboratory.value_review_registry import (
__all__ = [
"LABORATORY_EVIDENCE_DEFINITION_SCHEMA",
"LABORATORY_EVIDENCE_LIFECYCLE_DEFINITION_SCHEMA",
"LABORATORY_EVIDENCE_REPORT_SCHEMA",
"LaboratoryEvidenceDefinition",
"LaboratoryEvidenceRegistry",
"LaboratoryEvidenceVariant",
"LaboratoryEvidenceReportError",
"LaboratoryEvidenceReportNotFound",
"LaboratoryEvidenceReportService",
+128 -9
View File
@@ -7,13 +7,22 @@ from pathlib import Path, PurePosixPath
from typing import Final
LABORATORY_EVIDENCE_DEFINITION_SCHEMA: Final = "missioncore.laboratory-evidence-definition/v1"
LABORATORY_EVIDENCE_LIFECYCLE_DEFINITION_SCHEMA: Final = (
"missioncore.laboratory-evidence-definition/v2"
)
_DEFINITION_MAX_BYTES: Final = 16 * 1024
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9-]{2,95}$")
_SCHEMA_VERSION = re.compile(r"^missioncore\.[a-z0-9.-]+/v[1-9][0-9]*$")
_TOP_LEVEL_KEYS: Final = frozenset({"schema_version", "work_id", "evidence"})
_LIFECYCLE_TOP_LEVEL_KEYS: Final = frozenset(
{"schema_version", "work_id", "evidence_lifecycle"}
)
_EVIDENCE_KEYS: Final = frozenset(
{"runtime_relative_root", "result_id_prefix", "document_name", "schema_version"}
)
_LIFECYCLE_EVIDENCE_KEYS: Final = frozenset(
{"phase", "runtime_relative_root", "result_id_prefix", "document_name", "schema_version"}
)
class LaboratoryRegistryError(ValueError):
@@ -21,15 +30,15 @@ class LaboratoryRegistryError(ValueError):
@dataclass(frozen=True, slots=True)
class LaboratoryEvidenceDefinition:
work_id: str
class LaboratoryEvidenceVariant:
phase: str
runtime_relative_root: PurePosixPath
result_id_prefix: str
document_name: str
result_schema_version: str
def __post_init__(self) -> None:
_identifier(self.work_id, "work_id")
_identifier(self.phase, "evidence phase")
_identifier(self.result_id_prefix, "result_id_prefix")
_document_name(self.document_name)
_schema_version(self.result_schema_version)
@@ -45,6 +54,69 @@ class LaboratoryEvidenceDefinition:
return runtime_root.joinpath(*self.runtime_relative_root.parts)
@dataclass(frozen=True, slots=True)
class LaboratoryEvidenceDefinition:
work_id: str
runtime_relative_root: PurePosixPath
result_id_prefix: str
document_name: str
result_schema_version: str
lifecycle_variants: tuple[LaboratoryEvidenceVariant, ...] = ()
def __post_init__(self) -> None:
_identifier(self.work_id, "work_id")
primary = LaboratoryEvidenceVariant(
phase="result",
runtime_relative_root=self.runtime_relative_root,
result_id_prefix=self.result_id_prefix,
document_name=self.document_name,
result_schema_version=self.result_schema_version,
)
if not self.lifecycle_variants:
return
if not all(
isinstance(variant, LaboratoryEvidenceVariant)
for variant in self.lifecycle_variants
):
raise LaboratoryRegistryError("LAB lifecycle variants must be immutable evidence")
if self.lifecycle_variants[-1] != primary:
raise LaboratoryRegistryError("LAB lifecycle terminal evidence must be primary")
phases = [variant.phase for variant in self.lifecycle_variants]
if len(phases) != len(set(phases)):
raise LaboratoryRegistryError("duplicate LAB evidence phase")
@property
def evidence_variants(self) -> tuple[LaboratoryEvidenceVariant, ...]:
if self.lifecycle_variants:
return self.lifecycle_variants
return (
LaboratoryEvidenceVariant(
phase="result",
runtime_relative_root=self.runtime_relative_root,
result_id_prefix=self.result_id_prefix,
document_name=self.document_name,
result_schema_version=self.result_schema_version,
),
)
@property
def result_id_pattern(self) -> re.Pattern[str]:
return re.compile(rf"^{re.escape(self.result_id_prefix)}-[a-f0-9]{{64}}$")
def result_root(self, runtime_root: Path) -> Path:
return runtime_root.joinpath(*self.runtime_relative_root.parts)
def variant_for_result_id(self, result_id: str) -> LaboratoryEvidenceVariant | None:
return next(
(
variant
for variant in self.evidence_variants
if variant.result_id_pattern.fullmatch(result_id) is not None
),
None,
)
@dataclass(frozen=True, slots=True)
class LaboratoryEvidenceRegistry:
definitions: tuple[LaboratoryEvidenceDefinition, ...]
@@ -89,14 +161,42 @@ def _read_definition(path: Path) -> LaboratoryEvidenceDefinition:
except (json.JSONDecodeError, OSError) as exc:
raise LaboratoryRegistryError(f"LAB definition is unreadable: {path.name}") from exc
document = _object(payload, f"LAB definition {path.name}")
_exact_keys(document, _TOP_LEVEL_KEYS, f"LAB definition {path.name}")
if document["schema_version"] != LABORATORY_EVIDENCE_DEFINITION_SCHEMA:
schema_version = document.get("schema_version")
if schema_version not in {
LABORATORY_EVIDENCE_DEFINITION_SCHEMA,
LABORATORY_EVIDENCE_LIFECYCLE_DEFINITION_SCHEMA,
}:
raise LaboratoryRegistryError(f"LAB definition schema is invalid: {path.name}")
expected_keys = (
_TOP_LEVEL_KEYS
if schema_version == LABORATORY_EVIDENCE_DEFINITION_SCHEMA
else _LIFECYCLE_TOP_LEVEL_KEYS
)
_exact_keys(document, expected_keys, f"LAB definition {path.name}")
work_id = _identifier(document["work_id"], "work_id")
if path.name != f"{work_id}.json":
raise LaboratoryRegistryError(f"LAB definition filename must match work_id: {path.name}")
evidence = _object(document["evidence"], f"LAB evidence {work_id}")
_exact_keys(evidence, _EVIDENCE_KEYS, f"LAB evidence {work_id}")
if schema_version == LABORATORY_EVIDENCE_DEFINITION_SCHEMA:
lifecycle_variants: tuple[LaboratoryEvidenceVariant, ...] = ()
evidence = _object(document["evidence"], f"LAB evidence {work_id}")
_exact_keys(evidence, _EVIDENCE_KEYS, f"LAB evidence {work_id}")
else:
lifecycle = document["evidence_lifecycle"]
if not isinstance(lifecycle, list) or len(lifecycle) < 2:
raise LaboratoryRegistryError(
f"LAB evidence lifecycle must contain at least two phases: {work_id}"
)
lifecycle_variants = tuple(
_read_variant(row, f"LAB evidence {work_id}[{index}]")
for index, row in enumerate(lifecycle)
)
terminal = lifecycle_variants[-1]
evidence = {
"runtime_relative_root": str(terminal.runtime_relative_root),
"result_id_prefix": terminal.result_id_prefix,
"document_name": terminal.document_name,
"schema_version": terminal.result_schema_version,
}
result_id_prefix = _identifier(evidence["result_id_prefix"], "result_id_prefix")
document_name = _document_name(evidence["document_name"])
result_schema_version = _schema_version(evidence["schema_version"])
@@ -106,6 +206,19 @@ def _read_definition(path: Path) -> LaboratoryEvidenceDefinition:
result_id_prefix=result_id_prefix,
document_name=document_name,
result_schema_version=result_schema_version,
lifecycle_variants=lifecycle_variants,
)
def _read_variant(value: object, label: str) -> LaboratoryEvidenceVariant:
evidence = _object(value, label)
_exact_keys(evidence, _LIFECYCLE_EVIDENCE_KEYS, label)
return LaboratoryEvidenceVariant(
phase=_identifier(evidence["phase"], f"{label}.phase"),
runtime_relative_root=_relative_root(evidence["runtime_relative_root"]),
result_id_prefix=_identifier(evidence["result_id_prefix"], "result_id_prefix"),
document_name=_document_name(evidence["document_name"]),
result_schema_version=_schema_version(evidence["schema_version"]),
)
@@ -177,9 +290,15 @@ def _relative_root(value: object) -> PurePosixPath:
def _reject_duplicates(definitions: tuple[LaboratoryEvidenceDefinition, ...]) -> None:
dimensions = {
"work_id": [definition.work_id for definition in definitions],
"result_id_prefix": [definition.result_id_prefix for definition in definitions],
"result_id_prefix": [
variant.result_id_prefix
for definition in definitions
for variant in definition.evidence_variants
],
"runtime_relative_root": [
str(definition.runtime_relative_root) for definition in definitions
str(variant.runtime_relative_root)
for definition in definitions
for variant in definition.evidence_variants
],
}
for label, values in dimensions.items():
+13 -10
View File
@@ -9,6 +9,7 @@ from typing import Any, Final
from k1link.laboratory.evidence_registry import (
LaboratoryEvidenceDefinition,
LaboratoryEvidenceRegistry,
LaboratoryEvidenceVariant,
)
LABORATORY_EVIDENCE_REPORT_SCHEMA: Final = "missioncore.laboratory-evidence-report/v1"
@@ -40,12 +41,13 @@ def verify_laboratory_evidence_result(
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise LaboratoryEvidenceReportError("LAB evidence result is unavailable") from exc
if not resolved.is_dir() or definition.result_id_pattern.fullmatch(resolved.name) is None:
variant = definition.variant_for_result_id(resolved.name)
if not resolved.is_dir() or variant is None:
raise LaboratoryEvidenceReportError("LAB evidence result path is invalid")
document_path = _safe_file(resolved, definition.document_name)
document_path = _safe_file(resolved, variant.document_name)
document_bytes = _read_bounded(document_path, _DOCUMENT_MAX_BYTES, "LAB document")
document = _json_object(document_bytes, "LAB document")
_validate_document(document, definition, resolved.name)
_validate_document(document, variant, resolved.name)
identity = _object_or_none(document.get("identity"))
identity_sha256 = document.get("identity_sha256")
if identity is None or not isinstance(identity_sha256, str):
@@ -77,13 +79,14 @@ class LaboratoryEvidenceReportService:
def read(self, work_id: str, result_id: str) -> dict[str, object]:
definition = self._definitions.get(work_id)
if definition is None or definition.result_id_pattern.fullmatch(result_id) is None:
variant = definition.variant_for_result_id(result_id) if definition is not None else None
if definition is None or variant is None:
raise LaboratoryEvidenceReportNotFound("LAB evidence identity is unknown")
result_root = self._result_root(definition, result_id)
document_path = _safe_file(result_root, definition.document_name)
result_root = self._result_root(variant, result_id)
document_path = _safe_file(result_root, variant.document_name)
document_bytes = _read_bounded(document_path, _DOCUMENT_MAX_BYTES, "LAB document")
document = _json_object(document_bytes, "LAB document")
_validate_document(document, definition, result_id)
_validate_document(document, variant, result_id)
identity = _object_or_none(document.get("identity"))
identity_sha256 = document.get("identity_sha256")
@@ -210,7 +213,7 @@ class LaboratoryEvidenceReportService:
def _result_root(
self,
definition: LaboratoryEvidenceDefinition,
variant: LaboratoryEvidenceVariant,
result_id: str,
) -> Path:
configured = self._runtime_root_provider()
@@ -223,7 +226,7 @@ class LaboratoryEvidenceReportService:
runtime_root = runtime_root.resolve(strict=True)
except OSError as exc:
raise LaboratoryEvidenceReportNotFound("LAB runtime root is unavailable") from exc
candidate = definition.result_root(runtime_root) / result_id
candidate = variant.result_root(runtime_root) / result_id
if candidate.is_symlink():
raise LaboratoryEvidenceReportError("LAB result must not be a symlink")
try:
@@ -237,7 +240,7 @@ class LaboratoryEvidenceReportService:
def _validate_document(
document: dict[str, Any],
definition: LaboratoryEvidenceDefinition,
definition: LaboratoryEvidenceVariant,
result_id: str,
) -> None:
if document.get("schema_version") != definition.result_schema_version:
+44 -4
View File
@@ -169,10 +169,11 @@ class LaboratoryExecutionRegistry:
f"laboratory classification is incomplete; missing={missing}, unknown={unknown}"
)
for definition in self.definitions:
if (
evidence_by_work_id[definition.work_id].result_schema_version
!= definition.evidence_contract
):
evidence_contracts = {
variant.result_schema_version
for variant in evidence_by_work_id[definition.work_id].evidence_variants
}
if definition.evidence_contract not in evidence_contracts:
raise LaboratoryExecutionError(
f"laboratory evidence contract mismatch: {definition.work_id}"
)
@@ -311,6 +312,10 @@ class LaboratoryRunner:
def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
return {
"canonical.m48-small-static-passage-regression/v1": (
_run_m48_small_static_passage_regression
),
"canonical.m48-object-centric-quality/v1": _run_m48_object_centric_quality,
"canonical.m4-replay-threat/v1": _run_m4_replay_threat,
"canonical.e33-worker-shadow/v1": _run_e33,
"canonical.e35-degradation-recovery/v1": _run_e35,
@@ -319,6 +324,41 @@ def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
}
def _run_m48_small_static_passage_regression(
request: LaboratoryRunRequest,
) -> LaboratoryAdapterResult:
from k1link.laboratory.m48_small_static_regression import (
build_m48_small_static_passage_regression,
)
result = build_m48_small_static_passage_regression(
pack_root=request.inputs["pack_root"],
correction_session_path=request.inputs["correction_session_path"],
profile_path=request.inputs["profile_path"],
output_root=request.output_root,
)
return LaboratoryAdapterResult(
result_root=result.result_root,
result_id=result.result_id,
)
def _run_m48_object_centric_quality(
request: LaboratoryRunRequest,
) -> LaboratoryAdapterResult:
from k1link.laboratory.m48_object_quality import score_m48_object_quality
result = score_m48_object_quality(
pack_root=request.inputs["pack_root"],
truth_seal_root=request.inputs["truth_seal_root"],
output_root=request.output_root,
)
return LaboratoryAdapterResult(
result_root=result.result_root,
result_id=result.result_id,
)
def _run_m4_replay_threat(request: LaboratoryRunRequest) -> LaboratoryAdapterResult:
from k1link.perception.threat_replay import build_threat_replay
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,567 @@
"""Deterministic RAVNOVES00 adapter for the M4.8 object-quality pack."""
from __future__ import annotations
import hashlib
import json
import math
import re
from collections.abc import Iterator, Mapping
from itertools import zip_longest
from pathlib import Path
from typing import Any, Final
from k1link.laboratory.m47_reference_graph import read_m47_reference_graph_lab
from k1link.laboratory.m48_object_quality import (
M48_PREPARATION_PROVENANCE_SCHEMA,
M48_SELECTION_HYPOTHESIS_PROFILE,
M48ObjectQualityPack,
build_m48_object_quality_pack,
)
M48_SELECTION_SCHEMA: Final = "missioncore.m48-object-quality-selection/v1"
M48_SELECTION_ID: Final = "m48-ravnoves00-balanced-connected-clips/v1"
M48_SOURCE_ID: Final = "RAVNOVES00"
M48_SOURCE_SESSION_ID: Final = "20260720T065719Z_viewer_live"
M48_FRAME_COUNT: Final = 4_489
M48_IMAGE_WIDTH: Final = 800
M48_IMAGE_HEIGHT: Final = 600
_CAMERA_INDEX_SCHEMA: Final = "missioncore.camera-recording-index/v1"
_GRAPH_FRAME_SCHEMA: Final = "missioncore.local-obstacle-map/v1"
_THREAT_FRAME_SCHEMAS: Final = frozenset(
{
"missioncore.perception-threat-replay-frame/v1",
"missioncore.perception-threat-replay-frame/v2",
}
)
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
class M48Ravnoves00PackError(RuntimeError):
"""The source adapter escaped the accepted immutable RAVNOVES00 evidence."""
def prepare_m48_ravnoves00_pack(
*,
m47_lab_root: Path,
graph_result_root: Path,
threat_result_root: Path,
geometry_result_root: Path,
camera_index_path: Path,
selection_path: Path,
frozen_at_utc: str,
output_root: Path,
) -> M48ObjectQualityPack:
"""Freeze the selected M4.8 clips from the exact accepted M4.7 source."""
lab = read_m47_reference_graph_lab(m47_lab_root)
source = _mapping(lab.report.get("source"), "M4.7 source")
graph_root = _directory(graph_result_root, "M4.7 graph result")
threat_root = _directory(threat_result_root, "M4.6 visual result")
geometry_root = _directory(geometry_result_root, "M4.4 geometry result")
camera_index = _file(camera_index_path, "recorded camera index")
selection = _read_json(_file(selection_path, "M4.8 selection"), "M4.8 selection")
clips = _selection_clips(selection)
if (
graph_root.name != source.get("graph_result_id")
or threat_root.name != source.get("visual_result_id")
or source.get("source_id") != M48_SOURCE_ID
or source.get("source_session_id") != M48_SOURCE_SESSION_ID
):
raise M48Ravnoves00PackError("M4.8 source roots do not match the accepted M4.7 LAB")
graph_frames_path = _validate_graph_result(graph_root)
threat_frames_path, threat_identity = _validate_threat_result(
threat_root,
expected_frames_sha256=source.get("threat_frames_sha256"),
)
geometry_frames_path = _validate_geometry_result(
geometry_root,
expected_result_id=threat_identity.get("geometry_result_id"),
expected_frames_sha256=threat_identity.get("geometry_frames_sha256"),
)
camera_rows = tuple(_iter_jsonl(camera_index, "recorded camera index"))
_validate_camera_rows(camera_rows)
selected_sequences = {
sequence
for clip in clips
for sequence in range(
_integer(clip.get("start_sequence"), "clip start_sequence"),
_integer(clip.get("end_sequence"), "clip end_sequence") + 1,
)
}
frame_catalog: list[dict[str, object]] = []
predictions: list[dict[str, object]] = []
previous_source_time_ns = -1
graph_rows = _iter_jsonl(graph_frames_path, "M4.7 graph frames")
threat_rows = _iter_jsonl(threat_frames_path, "M4.6 threat frames")
geometry_rows = _iter_jsonl(geometry_frames_path, "M4.4 geometry frames")
for frame_index, values in enumerate(
zip_longest(graph_rows, threat_rows, geometry_rows, camera_rows),
):
graph_row, threat_row, geometry_row, camera_row = values
if graph_row is None or threat_row is None or geometry_row is None or camera_row is None:
raise M48Ravnoves00PackError("M4.8 source ledgers have different lengths")
sequence = frame_index + 1
source_time_ns = _validate_bound_frame(
graph_row=graph_row,
threat_row=threat_row,
geometry_row=geometry_row,
camera_row=camera_row,
frame_index=frame_index,
previous_source_time_ns=previous_source_time_ns,
)
previous_source_time_ns = source_time_ns
frame_catalog.append(
{
"sequence": sequence,
"source_time_ns": source_time_ns,
"camera_fragment_sha256": camera_row["sha256"],
}
)
if sequence in selected_sequences:
obstacle_map = _mapping(graph_row.get("obstacle_map"), "M4.7 obstacle map")
predictions.append(
{
"sequence": sequence,
"source_time_ns": source_time_ns,
"terminal_outcome": "delivered",
"terminal_reason": None,
"free_space_claimed": obstacle_map["free_space_claimed"],
"objects": _prediction_objects(
threat_row.get("camera_proposals"),
geometry_observations=geometry_row.get("observations"),
metric_obstacles=threat_row.get("metric_obstacles"),
),
}
)
if len(frame_catalog) != M48_FRAME_COUNT:
raise M48Ravnoves00PackError("M4.8 source frame count changed")
preparation_provenance = {
"schema_version": M48_PREPARATION_PROVENANCE_SCHEMA,
"adapter": {
"module": "k1link.laboratory.m48_ravnoves00_pack",
"sha256": _file_sha256(Path(__file__).resolve(strict=True)),
},
"selection": {
"selection_id": M48_SELECTION_ID,
"sha256": _file_sha256(selection_path),
},
"camera_index": {
"source_session_id": M48_SOURCE_SESSION_ID,
"sha256": _file_sha256(camera_index),
"byte_length": camera_index.stat().st_size,
"frame_count": len(camera_rows),
},
"graph": _source_provenance(graph_root, graph_frames_path),
"threat": _source_provenance(threat_root, threat_frames_path),
"geometry": _source_provenance(geometry_root, geometry_frames_path),
}
return build_m48_object_quality_pack(
m47_lab_root=lab.result_root,
frame_catalog=frame_catalog,
clips=clips,
predictions=predictions,
preparation_provenance=preparation_provenance,
frozen_at_utc=frozen_at_utc,
output_root=output_root,
)
def _validate_graph_result(root: Path) -> Path:
manifest = _read_json(_file(root / "manifest.json", "M4.7 graph manifest"), "graph manifest")
files = _mapping(manifest.get("files"), "M4.7 graph files")
descriptor = _mapping(files.get("frames.jsonl"), "M4.7 graph frame descriptor")
frames = _file(root / "frames.jsonl", "M4.7 graph frames")
expected_bytes = descriptor.get("bytes")
expected_sha256 = descriptor.get("sha256")
if (
manifest.get("schema_version") != "missioncore.reference-perception-graph-manifest/v1"
or manifest.get("result_id") != root.name
or manifest.get("accepted") is not True
or manifest.get("graph_id") != "reference-perception-graph/v2"
or manifest.get("run_mode") != "lossless-replay"
or not isinstance(expected_bytes, int)
or expected_bytes != frames.stat().st_size
or not _is_sha256(expected_sha256)
or _file_sha256(frames) != expected_sha256
):
raise M48Ravnoves00PackError("M4.7 graph result changed")
return frames
def _validate_threat_result(
root: Path,
*,
expected_frames_sha256: object,
) -> tuple[Path, dict[str, Any]]:
manifest = _read_json(
_file(root / "manifest.json", "M4.6 threat manifest"),
"threat manifest",
)
identity = _mapping(manifest.get("identity"), "M4.6 threat identity")
frames = _file(root / "frames.jsonl", "M4.6 threat frames")
if (
manifest.get("schema_version") != "missioncore.perception-threat-replay-result/v2"
or manifest.get("result_id") != root.name
or manifest.get("accepted") is not True
or identity.get("source_session_id") != M48_SOURCE_SESSION_ID
or not _is_sha256(expected_frames_sha256)
or identity.get("frames_sha256") != expected_frames_sha256
or _file_sha256(frames) != expected_frames_sha256
):
raise M48Ravnoves00PackError("M4.6 threat result changed")
return frames, identity
def _validate_geometry_result(
root: Path,
*,
expected_result_id: object,
expected_frames_sha256: object,
) -> Path:
manifest = _read_json(
_file(root / "manifest.json", "M4.4 geometry manifest"),
"geometry manifest",
)
identity = _mapping(manifest.get("identity"), "M4.4 geometry identity")
frames = _file(root / "frames.jsonl", "M4.4 geometry frames")
if (
manifest.get("schema_version") != "missioncore.perception-geometry-replay-result/v1"
or root.name != expected_result_id
or identity.get("accepted") is not True
or identity.get("source_pack_id")
!= "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
or not _is_sha256(expected_frames_sha256)
or identity.get("frames_sha256") != expected_frames_sha256
or _file_sha256(frames) != expected_frames_sha256
):
raise M48Ravnoves00PackError("M4.4 geometry result changed")
return frames
def _selection_clips(document: Mapping[str, object]) -> tuple[dict[str, object], ...]:
expected_keys = {
"schema_version",
"selection_id",
"source_id",
"source_session_id",
"selection_basis",
"camera_frame_size",
"selection_hypothesis_profile",
"clips",
}
frame_size = _mapping(document.get("camera_frame_size"), "selection frame size")
raw_clips = document.get("clips")
if (
set(document) != expected_keys
or document.get("schema_version") != M48_SELECTION_SCHEMA
or document.get("selection_id") != M48_SELECTION_ID
or document.get("source_id") != M48_SOURCE_ID
or document.get("source_session_id") != M48_SOURCE_SESSION_ID
or document.get("selection_basis")
!= "prediction-frozen-source-curation-before-independent-truth"
or frame_size != {"width": M48_IMAGE_WIDTH, "height": M48_IMAGE_HEIGHT}
or document.get("selection_hypothesis_profile") != M48_SELECTION_HYPOTHESIS_PROFILE
or not isinstance(raw_clips, list)
or any(not isinstance(item, dict) for item in raw_clips)
):
raise M48Ravnoves00PackError("M4.8 selection contract changed")
return tuple(dict(item) for item in raw_clips)
def _source_provenance(root: Path, frames_path: Path) -> dict[str, str]:
return {
"result_id": root.name,
"manifest_sha256": _file_sha256(root / "manifest.json"),
"frames_sha256": _file_sha256(frames_path),
}
def _validate_camera_rows(rows: tuple[dict[str, Any], ...]) -> None:
if len(rows) != M48_FRAME_COUNT:
raise M48Ravnoves00PackError("recorded camera index frame count changed")
previous_session_time = -1
for expected_sequence, row in enumerate(rows, start=1):
session_time = row.get("session_monotonic_ns")
if (
row.get("schema_version") != _CAMERA_INDEX_SCHEMA
or row.get("kind") != "media"
or row.get("sequence") != expected_sequence
or not isinstance(session_time, int)
or session_time <= previous_session_time
or not _is_sha256(row.get("sha256"))
):
raise M48Ravnoves00PackError("recorded camera index changed")
previous_session_time = session_time
def _validate_bound_frame(
*,
graph_row: Mapping[str, Any],
threat_row: Mapping[str, Any],
geometry_row: Mapping[str, Any],
camera_row: Mapping[str, Any],
frame_index: int,
previous_source_time_ns: int,
) -> int:
obstacle_map = _mapping(graph_row.get("obstacle_map"), "M4.7 obstacle map")
source_time_ns = threat_row.get("source_time_ns")
if (
graph_row.get("sequence") != frame_index
or obstacle_map.get("schema_version") != _GRAPH_FRAME_SCHEMA
or obstacle_map.get("frame_id") != f"frame-{frame_index:06d}"
or not isinstance(obstacle_map.get("free_space_claimed"), bool)
or threat_row.get("schema_version") not in _THREAT_FRAME_SCHEMAS
or threat_row.get("sequence") != frame_index
or threat_row.get("frame_id") != f"frame-{frame_index:06d}"
or geometry_row.get("schema_version") != "missioncore.perception-geometry-replay-frame/v1"
or geometry_row.get("sequence") != frame_index
or geometry_row.get("frame_id") != f"frame-{frame_index:06d}"
or geometry_row.get("source_available") != threat_row.get("source_available")
or not isinstance(source_time_ns, int)
or source_time_ns <= previous_source_time_ns
or camera_row.get("sequence") != frame_index + 1
):
raise M48Ravnoves00PackError("M4.8 frame binding changed")
return source_time_ns
def _prediction_objects(
value: object,
*,
geometry_observations: object,
metric_obstacles: object,
) -> list[dict[str, object]]:
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
raise M48Ravnoves00PackError("M4.6 camera proposal collection changed")
observations = _proposal_observations(geometry_observations)
obstacles = _metric_obstacles(metric_obstacles)
objects: list[dict[str, object]] = []
seen: set[str] = set()
for proposal in value:
prediction_id = proposal.get("proposal_id")
occupied_support = proposal.get("occupied_support")
threat_value = proposal.get("threat_decision")
if (
not isinstance(prediction_id, str)
or prediction_id in seen
or not isinstance(occupied_support, bool)
or threat_value not in {None, "threat", "not-threat", "unknown"}
):
raise M48Ravnoves00PackError("M4.6 camera proposal identity changed")
seen.add(prediction_id)
observation = observations.get(prediction_id)
geometry = "associated" if occupied_support else "unknown"
freshness = "current"
motion = "unsupported"
threat = threat_value if isinstance(threat_value, str) else "unknown"
if occupied_support:
if observation is None:
raise M48Ravnoves00PackError("associated proposal lost its geometry observation")
currentness = observation.get("currentness")
if currentness not in {"current", "held", "stale", "unavailable"}:
raise M48Ravnoves00PackError("associated proposal currentness changed")
freshness = str(currentness)
centroid = _metric_centroid(observation)
obstacle = _match_metric_obstacle(centroid, obstacles)
raw_motion = obstacle.get("motion")
motion = {
"moving": "moving",
"stationary": "static",
"unknown": "unknown",
}.get(str(raw_motion), "")
assessment = _mapping(obstacle.get("assessment"), "metric obstacle assessment")
obstacle_threat = assessment.get("decision")
if not motion or obstacle_threat not in {"threat", "not-threat", "unknown"}:
raise M48Ravnoves00PackError("associated proposal state changed")
threat = str(obstacle_threat)
causes: set[str] = set()
if geometry == "unknown":
causes.add("insufficient-geometry-support")
if threat == "unknown":
causes.add("threat-evidence-insufficient")
if motion == "unknown":
causes.add("motion-not-supported")
objects.append(
{
"prediction_id": prediction_id,
"extent_xyxy": _normalized_extent(proposal.get("bbox_xyxy")),
"geometry_association": geometry,
"freshness": freshness,
"motion": motion,
"threat": threat,
"unknown_causes": sorted(causes),
}
)
return objects
def _proposal_observations(value: object) -> dict[str, dict[str, Any]]:
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
raise M48Ravnoves00PackError("M4.4 observation collection changed")
mapped: dict[str, dict[str, Any]] = {}
for observation in value:
proposal_ids = observation.get("proposal_ids")
if not isinstance(proposal_ids, list) or any(
not isinstance(item, str) for item in proposal_ids
):
raise M48Ravnoves00PackError("M4.4 proposal binding changed")
for proposal_id in proposal_ids:
if proposal_id in mapped:
raise M48Ravnoves00PackError("M4.4 proposal has multiple observations")
mapped[proposal_id] = observation
return mapped
def _metric_obstacles(value: object) -> tuple[dict[str, Any], ...]:
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
raise M48Ravnoves00PackError("M4.6 metric obstacle collection changed")
return tuple(value)
def _metric_centroid(observation: Mapping[str, Any]) -> tuple[float, float, float]:
geometry = _mapping(observation.get("metric_geometry"), "proposal metric geometry")
value = geometry.get("centroid_xyz_m")
if (
not isinstance(value, list)
or len(value) != 3
or any(
not isinstance(item, (int, float))
or isinstance(item, bool)
or not math.isfinite(float(item))
for item in value
)
):
raise M48Ravnoves00PackError("proposal metric centroid changed")
return float(value[0]), float(value[1]), float(value[2])
def _match_metric_obstacle(
centroid: tuple[float, float, float],
obstacles: tuple[dict[str, Any], ...],
) -> dict[str, Any]:
matches: list[dict[str, Any]] = []
for obstacle in obstacles:
value = obstacle.get("centroid_map_xyz_m")
if (
isinstance(value, list)
and len(value) == 3
and all(isinstance(item, (int, float)) and not isinstance(item, bool) for item in value)
and max(
abs(float(left) - float(right)) for left, right in zip(value, centroid, strict=True)
)
<= 1e-9
):
matches.append(obstacle)
if len(matches) != 1:
raise M48Ravnoves00PackError("proposal metric obstacle association is ambiguous")
return matches[0]
def _normalized_extent(value: object) -> list[float]:
if (
not isinstance(value, list)
or len(value) != 4
or any(
not isinstance(item, (int, float))
or isinstance(item, bool)
or not math.isfinite(float(item))
for item in value
)
):
raise M48Ravnoves00PackError("M4.6 proposal extent changed")
x_min, y_min, x_max, y_max = (float(item) for item in value)
extent = [
x_min / M48_IMAGE_WIDTH,
y_min / M48_IMAGE_HEIGHT,
x_max / M48_IMAGE_WIDTH,
y_max / M48_IMAGE_HEIGHT,
]
if not 0.0 <= extent[0] < extent[2] <= 1.0 or not 0.0 <= extent[1] < extent[3] <= 1.0:
raise M48Ravnoves00PackError("M4.6 proposal extent escaped the camera raster")
return extent
def _iter_jsonl(path: Path, label: str) -> Iterator[dict[str, Any]]:
with path.open("r", encoding="utf-8") as stream:
for line_number, line in enumerate(stream, start=1):
try:
value = json.loads(line)
except json.JSONDecodeError as exc:
raise M48Ravnoves00PackError(f"{label} row {line_number} is invalid") from exc
if not isinstance(value, dict):
raise M48Ravnoves00PackError(f"{label} row {line_number} is not an object")
yield value
def _read_json(path: Path, label: str) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise M48Ravnoves00PackError(f"{label} is invalid") from exc
if not isinstance(value, dict):
raise M48Ravnoves00PackError(f"{label} must be an object")
return value
def _mapping(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise M48Ravnoves00PackError(f"{label} is invalid")
return value
def _integer(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool):
raise M48Ravnoves00PackError(f"{label} is invalid")
return value
def _directory(path: Path, label: str) -> Path:
candidate = path.expanduser().absolute()
if candidate.is_symlink():
raise M48Ravnoves00PackError(f"{label} must not be a symlink")
try:
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise M48Ravnoves00PackError(f"{label} is unavailable") from exc
if not resolved.is_dir():
raise M48Ravnoves00PackError(f"{label} is unavailable")
return resolved
def _file(path: Path, label: str) -> Path:
candidate = path.expanduser().absolute()
if candidate.is_symlink():
raise M48Ravnoves00PackError(f"{label} must not be a symlink")
try:
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise M48Ravnoves00PackError(f"{label} is unavailable") from exc
if not resolved.is_file():
raise M48Ravnoves00PackError(f"{label} is unavailable")
return resolved
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _is_sha256(value: object) -> bool:
return isinstance(value, str) and _SHA256.fullmatch(value) is not None
__all__ = [
"M48Ravnoves00PackError",
"M48_SELECTION_ID",
"M48_SELECTION_SCHEMA",
"prepare_m48_ravnoves00_pack",
]
+507
View File
@@ -0,0 +1,507 @@
"""Prediction-free raw spatial evidence for the neutral M4.8 review surface.
This reader deliberately does not open the M4.7 graph payload or the frozen M4.8
prediction ledger. It reuses the already verified recorded-geometry and replay
body-frame primitives to expose only a bounded current LiDAR increment in the
virtual body frame, together with immutable rig/corridor parameters.
"""
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from threading import RLock
from typing import Any, Final
from k1link.laboratory.m47_reference_graph import (
M47ReferenceGraphLabError,
read_m47_reference_graph_lab,
)
from k1link.laboratory.m48_object_quality import M48ObjectQualityPack
from k1link.perception.spatial_evidence import (
SpatialEvidenceProjectionError,
sample_points_in_body_frame,
)
from k1link.perception.threat_replay import (
ThreatReplayError,
ThreatReplayResult,
read_threat_replay_result,
)
from k1link.perception.threat_timeline import (
RECORDED_SPATIAL_POINT_LIMIT,
RecordedThreatTimeline,
RecordedThreatTimelineError,
)
M48_RAW_SPATIAL_FRAME_SCHEMA: Final = "missioncore.m48-neutral-object-review-spatial-frame/v1"
M48_EXPECTED_SOURCE_ID: Final = "RAVNOVES00"
M48_EXPECTED_E10_SOURCE_ID: Final = "sensor.camera.right"
M48_EXPECTED_SESSION_ID: Final = "20260720T065719Z_viewer_live"
M48_EXPECTED_FRAME_COUNT: Final = 4_489
M48_EXPECTED_SOURCE_PACK_ID: Final = (
"e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
)
M48_EXPECTED_SOURCE_PACK_SHA256: Final = (
"0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
)
M48_EXPECTED_THREAT_RESULT_ID: Final = (
"m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324"
)
_E10_SCHEMA: Final = "missioncore.e10-lidar-replay-pack/v1"
_E10_ARTIFACT_NAME: Final = "lidar-pack.npz"
_FALSE_AUTHORITY: Final = {
"mode": "replay-simulated",
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}
_MANIFEST_KEYS: Final = {
"artifact",
"classification",
"created_at_utc",
"ground_truth",
"identity",
"identity_sha256",
"pack_id",
"schema_version",
}
_IDENTITY_KEYS: Final = {
"available_lidar_frames",
"calibration_sha256",
"camera_slot",
"e6_profile_sha256",
"e6_result_id",
"frame_count",
"input_sha256",
"job_id",
"point_count",
"producer_sha256",
"projection",
"schema_version",
"semantic_timeline_result_id",
"session_id",
"source_end_frame_index",
"source_id",
"source_start_frame_index",
"temporal_binding",
"temporal_policy",
"timeline_end_seconds",
"timeline_start_seconds",
}
_ARTIFACT_KEYS: Final = {"byte_length", "media_type", "path", "sha256"}
class M48RawEvidenceError(RuntimeError):
"""Neutral M4.8 spatial evidence escaped an immutable source binding."""
@dataclass(frozen=True, slots=True)
class _PackFrameBinding:
clip_id: str
source_time_ns: int
class M48RawEvidenceReader:
"""Provide one prediction-blind, bounded body-frame projection per call.
Construct production instances with :meth:`from_repository`. The object is
directly compatible with the M4.8 API provider callable:
``reader(pack, one_based_sequence)``.
"""
def __init__(
self,
*,
repository_root: Path,
threat_result: ThreatReplayResult,
timeline: RecordedThreatTimeline,
point_limit: int = RECORDED_SPATIAL_POINT_LIMIT,
) -> None:
if (
not isinstance(point_limit, int)
or isinstance(point_limit, bool)
or not 1 <= point_limit <= RECORDED_SPATIAL_POINT_LIMIT
):
raise M48RawEvidenceError("M4.8 raw evidence point limit is invalid")
self.repository_root = repository_root.resolve(strict=True)
self.threat_result = threat_result
self.timeline = timeline
self.point_limit = point_limit
self._pack_indices: dict[str, dict[int, _PackFrameBinding]] = {}
self._lock = RLock()
@classmethod
def from_repository(
cls,
*,
repository_root: Path,
threat_result_root: Path,
expected_source_pack_id: str = M48_EXPECTED_SOURCE_PACK_ID,
point_limit: int = RECORDED_SPATIAL_POINT_LIMIT,
) -> M48RawEvidenceReader:
"""Open the exact sealed M4 result and its exact E10 source generation.
``threat_result_root`` is the immutable result generation directory, not
the parent collection. No latest-by-mtime discovery is permitted.
"""
repository = _strict_directory(repository_root, "repository root")
if expected_source_pack_id != M48_EXPECTED_SOURCE_PACK_ID:
raise M48RawEvidenceError("M4.8 E10 pack id escaped the canonical binding")
threat_root = _strict_directory(threat_result_root, "threat result root")
try:
result = read_threat_replay_result(threat_root)
except (OSError, ValueError, ThreatReplayError) as exc:
raise M48RawEvidenceError("M4.8 threat result is invalid") from exc
_validate_threat_result(result, expected_source_pack_id=expected_source_pack_id)
pack_root = (
repository / ".runtime/compute-experiments/e10/lidar-packs" / expected_source_pack_id
)
_validate_e10_pack(
pack_root,
expected_pack_id=expected_source_pack_id,
expected_artifact_sha256=M48_EXPECTED_SOURCE_PACK_SHA256,
)
try:
timeline = RecordedThreatTimeline(repository_root=repository, result=result)
except (OSError, ValueError, RecordedThreatTimelineError) as exc:
raise M48RawEvidenceError("M4.8 recorded geometry timeline is invalid") from exc
if (
len(timeline.index.source_times_ns) != M48_EXPECTED_FRAME_COUNT
or timeline.profile.source_id != M48_EXPECTED_SOURCE_ID
or timeline.profile.session_id != M48_EXPECTED_SESSION_ID
or timeline.profile.source_pack_id != expected_source_pack_id
or timeline.profile.source_pack_sha256 != M48_EXPECTED_SOURCE_PACK_SHA256
):
raise M48RawEvidenceError("M4.8 recorded geometry binding changed")
return cls(
repository_root=repository,
threat_result=result,
timeline=timeline,
point_limit=point_limit,
)
def __call__(
self,
pack: M48ObjectQualityPack,
sequence: int,
) -> dict[str, object]:
return self.frame(pack=pack, sequence=sequence)
def frame(
self,
*,
pack: M48ObjectQualityPack,
sequence: int,
) -> dict[str, object]:
"""Return one one-based, clip-bound neutral spatial frame."""
if (
not isinstance(sequence, int)
or isinstance(sequence, bool)
or not 1 <= sequence <= M48_EXPECTED_FRAME_COUNT
):
raise M48RawEvidenceError("M4.8 raw evidence sequence is invalid")
binding = self._binding_for(pack, sequence)
frame_index = sequence - 1
try:
temporal = self.timeline.store.temporal_binding_for_index(frame_index)
body_frame = self.timeline.body_frames.body_frame_for_frame(f"frame-{frame_index:06d}")
except (RuntimeError, TypeError, ValueError) as exc:
raise M48RawEvidenceError("M4.8 source frame binding is invalid") from exc
if temporal.frame_index != frame_index or temporal.source_time_ns != binding.source_time_ns:
raise M48RawEvidenceError("M4.8 source time escaped the neutral frame reference")
points_body: list[list[float]] = []
if body_frame is not None:
if not temporal.source_available:
raise M48RawEvidenceError("unavailable source produced an M4.8 body frame")
points = self.timeline.store.current_points_for_frame(frame_index)
if points is None:
raise M48RawEvidenceError("qualified M4.8 body frame lacks current LiDAR")
try:
points_body, _ = sample_points_in_body_frame(
points,
body_frame,
point_limit=self.point_limit,
)
except SpatialEvidenceProjectionError as exc:
raise M48RawEvidenceError("M4.8 body-frame point projection failed") from exc
profile = self.timeline.profile
return {
"schema_version": M48_RAW_SPATIAL_FRAME_SCHEMA,
"pack_id": pack.result_id,
"clip_id": binding.clip_id,
"sequence": sequence,
"source_time_ns": temporal.source_time_ns,
"source_available": temporal.source_available,
"body_frame_available": body_frame is not None,
"point_cloud_body_xyz_m": points_body,
"rig": {
"profile_id": profile.rig.profile_id,
"length_m": profile.rig.body_length_m,
"width_m": profile.rig.body_width_m,
"lidar_reference": profile.rig.lidar_reference,
"nominal_sensor_height_m": profile.rig.nominal_sensor_height_m,
"physical_mount_claimed": False,
},
"corridor": {
"profile_id": profile.corridor.profile_id,
"forward_length_m": profile.corridor.forward_length_m,
"rear_margin_m": profile.corridor.rear_margin_m,
"lateral_clearance_m": profile.corridor.lateral_clearance_m,
"half_width_m": (
profile.rig.body_width_m / 2 + profile.corridor.lateral_clearance_m
),
"prediction_horizon_seconds": (profile.corridor.prediction_horizon_seconds),
},
"occupied_voxel_size_m": profile.corridor.occupied_voxel_size_m,
"candidate_identity_included": False,
"graph_boxes_ids_scores_included": False,
"frozen_predictions_included": False,
"strata_included": False,
"authority": dict(_FALSE_AUTHORITY),
}
def _binding_for(
self,
pack: M48ObjectQualityPack,
sequence: int,
) -> _PackFrameBinding:
with self._lock:
index = self._pack_indices.get(pack.result_id)
if index is None:
_validate_m47_pack_binding(
repository_root=self.repository_root,
pack=pack,
threat_result=self.threat_result,
)
index = _index_neutral_frame_references(pack)
self._pack_indices[pack.result_id] = index
binding = index.get(sequence)
if binding is None:
raise M48RawEvidenceError("M4.8 sequence is outside the selected neutral clips")
return binding
def _validate_threat_result(
result: ThreatReplayResult,
*,
expected_source_pack_id: str,
) -> None:
identity = result.manifest.get("identity")
metrics = identity.get("metrics") if isinstance(identity, dict) else None
frames = metrics.get("frames") if isinstance(metrics, dict) else None
if (
result.result_id != M48_EXPECTED_THREAT_RESULT_ID
or result.result_root.name != result.result_id
or result.accepted is not True
or not isinstance(identity, dict)
or identity.get("source_id") != M48_EXPECTED_SOURCE_ID
or identity.get("source_session_id") != M48_EXPECTED_SESSION_ID
or identity.get("source_pack_id") != expected_source_pack_id
or identity.get("source_pack_sha256") != M48_EXPECTED_SOURCE_PACK_SHA256
or not isinstance(frames, dict)
or frames.get("total") != M48_EXPECTED_FRAME_COUNT
or identity.get("authority")
!= {
**_FALSE_AUTHORITY,
"physical_collision_accepted": False,
"ground_truth": False,
}
):
raise M48RawEvidenceError("M4.8 threat result escaped the canonical source")
def _validate_e10_pack(
pack_root: Path,
*,
expected_pack_id: str,
expected_artifact_sha256: str,
) -> Path:
root = _strict_directory(pack_root, "E10 pack root")
if root.name != expected_pack_id:
raise M48RawEvidenceError("E10 pack path escaped its expected identity")
manifest_path = root / "manifest.json"
if (
manifest_path.is_symlink()
or not manifest_path.is_file()
or manifest_path.resolve(strict=True).parent != root
):
raise M48RawEvidenceError("E10 manifest path is invalid")
manifest = _read_json(manifest_path, "E10 manifest")
if set(manifest) != _MANIFEST_KEYS:
raise M48RawEvidenceError("E10 manifest fields changed")
identity = _mapping(manifest.get("identity"), "E10 identity")
artifact = _mapping(manifest.get("artifact"), "E10 artifact")
if set(identity) != _IDENTITY_KEYS or set(artifact) != _ARTIFACT_KEYS:
raise M48RawEvidenceError("E10 identity or artifact fields changed")
identity_sha256 = _canonical_sha256(identity)
if (
manifest.get("schema_version") != _E10_SCHEMA
or manifest.get("pack_id") != expected_pack_id
or manifest.get("identity_sha256") != identity_sha256
or expected_pack_id != f"e10-lidar-pack-{identity_sha256}"
or manifest.get("classification") != "private-recorded-sensor-replay-input"
or manifest.get("ground_truth") is not False
or identity.get("schema_version") != _E10_SCHEMA
or identity.get("source_id") != M48_EXPECTED_E10_SOURCE_ID
or identity.get("session_id") != M48_EXPECTED_SESSION_ID
or identity.get("frame_count") != M48_EXPECTED_FRAME_COUNT
or identity.get("source_start_frame_index") != 0
or identity.get("source_end_frame_index") != M48_EXPECTED_FRAME_COUNT - 1
or artifact.get("path") != _E10_ARTIFACT_NAME
or artifact.get("media_type") != "application/x-npz"
or artifact.get("sha256") != expected_artifact_sha256
):
raise M48RawEvidenceError("E10 pack identity changed")
byte_length = artifact.get("byte_length")
if not isinstance(byte_length, int) or isinstance(byte_length, bool) or byte_length < 1:
raise M48RawEvidenceError("E10 artifact byte length is invalid")
artifact_path = root / _E10_ARTIFACT_NAME
if (
artifact_path.is_symlink()
or not artifact_path.is_file()
or artifact_path.resolve(strict=True).parent != root
or artifact_path.stat().st_size != byte_length
or _file_sha256(artifact_path) != expected_artifact_sha256
):
raise M48RawEvidenceError("E10 artifact content changed")
return artifact_path.resolve(strict=True)
def _validate_m47_pack_binding(
*,
repository_root: Path,
pack: M48ObjectQualityPack,
threat_result: ThreatReplayResult,
) -> None:
identity = _mapping(pack.manifest.get("identity"), "M4.8 pack identity")
source = _mapping(identity.get("source"), "M4.8 pack source")
m47_id = source.get("m47_lab_result_id")
m47_manifest_sha256 = source.get("m47_lab_manifest_sha256")
if (
source.get("source_id") != M48_EXPECTED_SOURCE_ID
or source.get("source_session_id") != M48_EXPECTED_SESSION_ID
or not isinstance(m47_id, str)
or not isinstance(m47_manifest_sha256, str)
):
raise M48RawEvidenceError("M4.8 pack source binding changed")
m47_root = repository_root / ".runtime/compute-experiments/m47/reference-graph-labs" / m47_id
manifest_path = m47_root / "manifest.json"
if (
manifest_path.is_symlink()
or not manifest_path.is_file()
or _file_sha256(manifest_path) != m47_manifest_sha256
):
raise M48RawEvidenceError("M4.8 pack M4.7 manifest binding changed")
try:
m47 = read_m47_reference_graph_lab(m47_root)
except (OSError, ValueError, M47ReferenceGraphLabError) as exc:
raise M48RawEvidenceError("M4.8 pack M4.7 LAB is invalid") from exc
m47_source = _mapping(m47.report.get("source"), "M4.7 source")
threat_identity = _mapping(threat_result.manifest.get("identity"), "M4 threat identity")
if (
m47.manifest.get("accepted") is not True
or m47_source.get("source_id") != M48_EXPECTED_SOURCE_ID
or m47_source.get("source_session_id") != M48_EXPECTED_SESSION_ID
or m47_source.get("visual_result_id") != threat_result.result_id
or m47_source.get("threat_frames_sha256") != threat_identity.get("frames_sha256")
):
raise M48RawEvidenceError("M4.8 pack escaped its accepted M4.7 visual source")
def _index_neutral_frame_references(
pack: M48ObjectQualityPack,
) -> dict[int, _PackFrameBinding]:
index: dict[int, _PackFrameBinding] = {}
for raw in pack.frame_references:
row = _mapping(raw, "M4.8 neutral frame reference")
sequence = row.get("sequence")
source_time_ns = row.get("source_time_ns")
clip_id = row.get("clip_id")
if (
not isinstance(sequence, int)
or isinstance(sequence, bool)
or not 1 <= sequence <= M48_EXPECTED_FRAME_COUNT
or not isinstance(source_time_ns, int)
or isinstance(source_time_ns, bool)
or source_time_ns < 0
or not isinstance(clip_id, str)
or not clip_id
or sequence in index
):
raise M48RawEvidenceError("M4.8 neutral frame references are invalid")
index[sequence] = _PackFrameBinding(
clip_id=clip_id,
source_time_ns=source_time_ns,
)
if not index:
raise M48RawEvidenceError("M4.8 neutral frame reference set is empty")
return index
def _strict_directory(path: Path, label: str) -> Path:
candidate = path.expanduser().absolute()
if candidate.is_symlink():
raise M48RawEvidenceError(f"{label} must not be a symlink")
try:
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise M48RawEvidenceError(f"{label} is unavailable") from exc
if not resolved.is_dir():
raise M48RawEvidenceError(f"{label} is not a directory")
return resolved
def _read_json(path: Path, label: str) -> dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise M48RawEvidenceError(f"{label} is invalid") from exc
if not isinstance(value, dict):
raise M48RawEvidenceError(f"{label} is invalid")
return value
def _mapping(value: object, label: str) -> Mapping[str, Any]:
if not isinstance(value, dict):
raise M48RawEvidenceError(f"{label} is invalid")
return value
def _canonical_sha256(value: object) -> str:
return hashlib.sha256(
json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
).hexdigest()
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
while chunk := handle.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
__all__ = [
"M48_EXPECTED_FRAME_COUNT",
"M48_EXPECTED_SESSION_ID",
"M48_EXPECTED_SOURCE_ID",
"M48_EXPECTED_SOURCE_PACK_ID",
"M48_EXPECTED_THREAT_RESULT_ID",
"M48_RAW_SPATIAL_FRAME_SCHEMA",
"M48RawEvidenceError",
"M48RawEvidenceReader",
]
@@ -0,0 +1,711 @@
"""Immutable M4.8 development regression over operator-added missed-object anchors.
The experiment deliberately stays inside M4.8 and reuses the frozen Worker 006
prediction pack. It snapshots only operator-added tracklets from reviewed clips,
compares the exact source frame against the already-frozen prediction row, and
publishes a separate append-only result. The assisted correction is never called
independent truth and the result grants no navigation or safety authority.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import shutil
import uuid
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final
from k1link.laboratory.m48_object_quality import (
M48ObjectQualityError,
read_m48_object_quality_pack,
)
M48_SMALL_STATIC_PROFILE_SCHEMA: Final = (
"missioncore.m48-small-static-passage-regression-profile/v1"
)
M48_SMALL_STATIC_RESULT_SCHEMA: Final = (
"missioncore.m48-small-static-passage-regression-result/v1"
)
M48_SMALL_STATIC_REPORT_SCHEMA: Final = (
"missioncore.m48-small-static-passage-regression-report/v1"
)
M48_SMALL_STATIC_ANCHOR_SCHEMA: Final = (
"missioncore.m48-assisted-missed-object-anchor/v1"
)
M48_SMALL_STATIC_COMPARISON_SCHEMA: Final = (
"missioncore.m48-assisted-anchor-comparison/v1"
)
M48_SMALL_STATIC_PREFIX: Final = "m48-small-static-passage-regression-"
_CORRECTION_SCHEMA: Final = "missioncore.m48-assisted-object-correction-session/v1"
_METHOD_SCHEMA: Final = "missioncore.laboratory-method/v1"
_OBJECT_ID = re.compile(r"^object-[0-9]{2,}$")
_AUTHORITY: Final = {
"mode": "replay-simulated",
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}
class M48SmallStaticRegressionError(RuntimeError):
"""The assisted development-regression source or result is invalid."""
@dataclass(frozen=True, slots=True)
class M48SmallStaticRegressionResult:
result_id: str
result_root: Path
manifest: dict[str, Any]
report: dict[str, Any]
anchors: tuple[dict[str, Any], ...]
comparisons: tuple[dict[str, Any], ...]
def build_m48_small_static_passage_regression(
*,
pack_root: Path,
correction_session_path: Path,
profile_path: Path,
output_root: Path,
run_created_at_utc: str | None = None,
) -> M48SmallStaticRegressionResult:
"""Publish one append-only M4.8R development baseline without mutating inputs."""
try:
pack = read_m48_object_quality_pack(pack_root)
except M48ObjectQualityError as exc:
raise M48SmallStaticRegressionError("M4.8 frozen prediction pack is invalid") from exc
profile_bytes, profile = _read_profile(profile_path)
correction_bytes, correction = _read_correction(correction_session_path, pack.result_id)
anchors = _assisted_anchors(correction)
if len(anchors) < int(profile["minimum_anchor_count"]):
raise M48SmallStaticRegressionError("M4.8 assisted anchor set is too small")
prediction_rows: dict[tuple[str, int], dict[str, Any]] = {}
for row in pack.predictions:
clip_id = row.get("clip_id")
sequence = row.get("sequence")
if not isinstance(clip_id, str) or not _integer(sequence):
raise M48SmallStaticRegressionError("M4.8 frozen prediction binding is invalid")
key = (clip_id, int(sequence))
if key in prediction_rows:
raise M48SmallStaticRegressionError("M4.8 frozen prediction binding collided")
prediction_rows[key] = row
threshold = float(profile["extent_iou_threshold"])
comparisons = tuple(
_compare_anchor(anchor, prediction_rows, threshold)
for anchor in anchors
)
recalled = sum(bool(row["matched_at_threshold"]) for row in comparisons)
recall = recalled / len(comparisons)
passage_count = sum(bool(row["requires_avoidance_or_clearance"]) for row in anchors)
clip_count = len({str(row["clip_id"]) for row in anchors})
target = float(profile["minimum_assisted_anchor_recall"])
accepted = recall >= target
created_at = _utc_timestamp(run_created_at_utc or datetime.now(UTC).isoformat())
correction_sha256 = hashlib.sha256(correction_bytes).hexdigest()
profile_sha256 = hashlib.sha256(profile_bytes).hexdigest()
producer_sha256 = _file_sha256(Path(__file__).resolve())
pack_identity = _object(pack.manifest.get("identity"), "M4.8 pack identity")
freeze = _object(pack_identity.get("freeze"), "M4.8 pack freeze")
identity: dict[str, Any] = {
"schema_version": M48_SMALL_STATIC_RESULT_SCHEMA,
"human_lab_id": profile["human_lab_id"],
"run_label": profile["run_label"],
"run_created_at_utc": created_at,
"pipeline_id": profile["pipeline_id"],
"experiment_id": profile["experiment_id"],
"profile_id": profile["profile_id"],
"profile_sha256": profile_sha256,
"producer_sha256": producer_sha256,
"source": {
"source_id": _object(
pack_identity.get("source"), "M4.8 source"
).get("source_id"),
"source_session_id": _object(
pack_identity.get("source"), "M4.8 source"
).get("source_session_id"),
"pack_id": pack.result_id,
"pack_identity_sha256": pack.manifest["identity_sha256"],
"prediction_rows_sha256": freeze.get("prediction_rows_sha256"),
"correction_session_id": correction["session_id"],
"correction_revision": correction["revision"],
"correction_updated_at_utc": correction["updated_at_utc"],
"correction_document_sha256": correction_sha256,
"correction_independent_truth": False,
},
"selection": {
"anchor_selection": profile["anchor_selection"],
"assisted_tracklet_count": len({(row["clip_id"], row["object_id"]) for row in anchors}),
"anchor_count": len(anchors),
"clip_count": clip_count,
"requires_avoidance_or_clearance_count": passage_count,
},
"authority": dict(_AUTHORITY),
}
identity_sha256 = _canonical_sha256(identity)
result_id = f"{M48_SMALL_STATIC_PREFIX}{identity_sha256}"
method = {
"schema_version": _METHOD_SCHEMA,
"completeness": "complete",
"execution_class": "deterministic",
"pipeline_id": profile["pipeline_id"],
"components": [
{
"kind": "source",
"name": "M4.8 frozen Worker 006 predictions",
"version": pack.result_id,
"role": "immutable candidate rows from the current M4.8 pipeline",
"identity_sha256": freeze.get("prediction_rows_sha256"),
},
{
"kind": "source",
"name": "operator-added missed-object anchors",
"version": f"{correction['session_id']}:revision-{correction['revision']}",
"role": "assisted development regression seed; not independent truth",
"identity_sha256": correction_sha256,
},
{
"kind": "algorithm",
"name": "exact-frame class-free IoU comparator",
"version": profile["profile_id"],
"role": "diagnostic detection recall over operator-added anchors",
"identity_sha256": producer_sha256,
},
],
}
metrics = {
"assisted_anchor_count": len(comparisons),
"assisted_tracklet_count": len({(row["clip_id"], row["object_id"]) for row in anchors}),
"anchor_clip_count": clip_count,
"requires_avoidance_or_clearance_count": passage_count,
"worker_recalled_anchor_count": recalled,
"worker_missed_anchor_count": len(comparisons) - recalled,
"assisted_anchor_recall": recall,
"extent_iou_threshold": threshold,
"minimum_assisted_anchor_recall": target,
}
gates = {
"anchor_set_non_empty": len(comparisons) >= int(profile["minimum_anchor_count"]),
"development_anchor_recall_target": accepted,
"independent_truth_available": False,
}
decision = {
"state": (
"accepted-development-regression-baseline"
if accepted
else "failed-development-regression-baseline"
),
"summary": (
f"Worker 006 matched {recalled}/{len(comparisons)} exact-frame assisted anchors "
f"at IoU >= {threshold:.2f}."
),
"next_action": (
"Keep the pipeline contract fixed, change only the perception experiment, "
"and publish another immutable M4.8R run against this frozen seed."
),
}
limitations = [
"The anchors come from candidate-visible operator correction and are not "
"independent truth.",
"The seed is intentionally biased toward objects the current Worker 006 output missed.",
"A camera rectangle is evidence of a missed visible object, not a measured 3D collider.",
"No physical-live, navigation, command, actuation or collision-safety "
"authority is granted.",
]
report = {
"schema_version": M48_SMALL_STATIC_REPORT_SCHEMA,
"result_id": result_id,
"source": identity["source"],
"configuration": {
**profile,
"profile_sha256": profile_sha256,
},
"method": method,
"execution": {
"comparison_node": "mission-core-local-control-plane",
"source_worker_id": "006",
"frozen_prediction_rows_sha256": freeze.get("prediction_rows_sha256"),
"determinism": "exact canonical JSON + exact-frame IoU; no inference rerun",
},
"metrics": metrics,
"gates": gates,
"decision": decision,
"limitations": limitations,
"authority": dict(_AUTHORITY),
"visual_review": {
"viewer": "missioncore.laboratory-recorded-clip-viewer/v1",
"case_count": len(comparisons),
"camera_anchor_and_worker_boxes": True,
"camera_3d_plan_shared_clock": True,
},
}
destination = output_root.expanduser().absolute() / result_id
_publish_result(
destination=destination,
identity=identity,
created_at_utc=created_at,
accepted=accepted,
report=report,
anchors=anchors,
comparisons=comparisons,
)
return read_m48_small_static_passage_regression(destination)
def read_m48_small_static_passage_regression(
root: Path,
) -> M48SmallStaticRegressionResult:
candidate = root.expanduser().absolute()
if candidate.is_symlink():
raise M48SmallStaticRegressionError("M4.8 regression result must not be a symlink")
try:
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise M48SmallStaticRegressionError("M4.8 regression result is unavailable") from exc
if not resolved.is_dir() or not resolved.name.startswith(M48_SMALL_STATIC_PREFIX):
raise M48SmallStaticRegressionError("M4.8 regression result path is invalid")
manifest = _read_json(resolved / "manifest.json", maximum=1024 * 1024)
identity = _object(manifest.get("identity"), "M4.8 regression identity")
identity_sha256 = _canonical_sha256(identity)
if (
manifest.get("schema_version") != M48_SMALL_STATIC_RESULT_SCHEMA
or manifest.get("result_id") != resolved.name
or manifest.get("identity_sha256") != identity_sha256
or resolved.name != f"{M48_SMALL_STATIC_PREFIX}{identity_sha256}"
or manifest.get("ground_truth") is not False
or manifest.get("authority") != _AUTHORITY
):
raise M48SmallStaticRegressionError("M4.8 regression identity changed")
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list) or len(artifacts) != 3:
raise M48SmallStaticRegressionError("M4.8 regression artifact inventory changed")
by_path: dict[str, dict[str, Any]] = {}
for raw in artifacts:
descriptor = _object(raw, "M4.8 regression artifact")
path_name = descriptor.get("path")
if not isinstance(path_name, str) or path_name not in {
"anchors.jsonl", "comparisons.jsonl", "report.json"
} or path_name in by_path:
raise M48SmallStaticRegressionError("M4.8 regression artifact path changed")
path = resolved / path_name
if (
path.is_symlink()
or not path.is_file()
or descriptor.get("byte_length") != path.stat().st_size
or descriptor.get("sha256") != _file_sha256(path)
):
raise M48SmallStaticRegressionError("M4.8 regression artifact proof changed")
by_path[path_name] = descriptor
report = _read_json(resolved / "report.json", maximum=1024 * 1024)
anchors = tuple(_read_jsonl(resolved / "anchors.jsonl"))
comparisons = tuple(_read_jsonl(resolved / "comparisons.jsonl"))
if (
report.get("schema_version") != M48_SMALL_STATIC_REPORT_SCHEMA
or report.get("result_id") != resolved.name
or len(anchors) != len(comparisons)
or any(row.get("schema_version") != M48_SMALL_STATIC_ANCHOR_SCHEMA for row in anchors)
or any(
row.get("schema_version") != M48_SMALL_STATIC_COMPARISON_SCHEMA
for row in comparisons
)
or [row.get("anchor_id") for row in anchors]
!= [row.get("anchor_id") for row in comparisons]
):
raise M48SmallStaticRegressionError("M4.8 regression content changed")
return M48SmallStaticRegressionResult(
result_id=resolved.name,
result_root=resolved,
manifest=manifest,
report=report,
anchors=anchors,
comparisons=comparisons,
)
def _read_profile(path: Path) -> tuple[bytes, dict[str, Any]]:
encoded, profile = _read_json_bytes(path, maximum=64 * 1024, label="M4.8 regression profile")
expected = {
"schema_version",
"profile_id",
"pipeline_id",
"experiment_id",
"human_lab_id",
"run_label",
"anchor_selection",
"extent_iou_threshold",
"minimum_assisted_anchor_recall",
"minimum_anchor_count",
"independent_truth",
}
if set(profile) != expected or profile.get("schema_version") != M48_SMALL_STATIC_PROFILE_SCHEMA:
raise M48SmallStaticRegressionError("M4.8 regression profile contract changed")
if (
profile.get("human_lab_id") != "M4.8"
or profile.get("anchor_selection") != "operator-added-tracklets-in-reviewed-clips/v1"
or profile.get("independent_truth") is not False
or not _rate(profile.get("extent_iou_threshold"))
or not _rate(profile.get("minimum_assisted_anchor_recall"))
or not _integer(profile.get("minimum_anchor_count"))
or int(profile["minimum_anchor_count"]) < 1
):
raise M48SmallStaticRegressionError("M4.8 regression profile is invalid")
for key in ("profile_id", "pipeline_id", "experiment_id", "run_label"):
if not isinstance(profile.get(key), str) or not str(profile[key]).strip():
raise M48SmallStaticRegressionError("M4.8 regression profile identity is invalid")
return encoded, profile
def _read_correction(path: Path, pack_id: str) -> tuple[bytes, dict[str, Any]]:
encoded, correction = _read_json_bytes(
path,
maximum=16 * 1024 * 1024,
label="M4.8 correction snapshot",
)
assistance = _object(correction.get("assistance"), "M4.8 correction assistance")
if (
correction.get("schema_version") != _CORRECTION_SCHEMA
or correction.get("pack_id") != pack_id
or correction.get("state") not in {"saved", "frozen"}
or not _integer(correction.get("revision"))
or int(correction["revision"]) < 1
or not isinstance(correction.get("session_id"), str)
or not isinstance(correction.get("updated_at_utc"), str)
or assistance.get("candidate_predictions_seen") is not True
or assistance.get("independent_truth_eligible") is not False
or correction.get("authority") != _AUTHORITY
or not isinstance(correction.get("clips"), list)
):
raise M48SmallStaticRegressionError("M4.8 correction snapshot is invalid")
return encoded, correction
def _assisted_anchors(correction: dict[str, Any]) -> tuple[dict[str, Any], ...]:
anchors: list[dict[str, Any]] = []
for clip_raw in correction["clips"]:
clip = _object(clip_raw, "M4.8 correction clip")
if clip.get("review_state") != "reviewed":
continue
clip_id = clip.get("clip_id")
tracklets = clip.get("tracklets")
if not isinstance(clip_id, str) or not isinstance(tracklets, list):
raise M48SmallStaticRegressionError("M4.8 correction clip is invalid")
for tracklet_raw in tracklets:
tracklet = _object(tracklet_raw, "M4.8 correction tracklet")
object_id = tracklet.get("object_id")
if not isinstance(object_id, str) or _OBJECT_ID.fullmatch(object_id) is None:
continue
keyframes = tracklet.get("keyframes")
if not isinstance(keyframes, list) or not keyframes:
raise M48SmallStaticRegressionError("M4.8 assisted tracklet has no keyframes")
for keyframe_raw in keyframes:
keyframe = _object(keyframe_raw, "M4.8 correction keyframe")
sequence = keyframe.get("sequence")
extent = _extent(keyframe.get("extent_xyxy"))
if not _integer(sequence):
raise M48SmallStaticRegressionError("M4.8 assisted anchor sequence is invalid")
state = _state_for_sequence(tracklet, int(sequence))
anchor_identity = {
"clip_id": clip_id,
"object_id": object_id,
"sequence": int(sequence),
"extent_xyxy": extent,
}
anchors.append({
"schema_version": M48_SMALL_STATIC_ANCHOR_SCHEMA,
"anchor_id": "anchor-" + _canonical_sha256(anchor_identity)[:24],
**anchor_identity,
"visibility": keyframe.get("visibility"),
"geometry_association": state.get("geometry_association"),
"freshness": state.get("freshness"),
"motion": state.get("motion"),
"threat": state.get("threat"),
"requires_avoidance_or_clearance": bool(
state.get("critical_corridor_obstacle")
),
"authority": "operator-assisted-development-anchor-not-truth",
})
anchors.sort(key=lambda row: (str(row["clip_id"]), int(row["sequence"]), str(row["object_id"])))
if len({str(row["anchor_id"]) for row in anchors}) != len(anchors):
raise M48SmallStaticRegressionError("M4.8 assisted anchor identity collided")
return tuple(anchors)
def _state_for_sequence(tracklet: dict[str, Any], sequence: int) -> dict[str, Any]:
segments = tracklet.get("state_segments")
if not isinstance(segments, list):
raise M48SmallStaticRegressionError("M4.8 assisted state segments are invalid")
matches = [
_object(row, "M4.8 assisted state segment")
for row in segments
if isinstance(row, dict)
and _integer(row.get("start_sequence"))
and _integer(row.get("end_sequence"))
and int(row["start_sequence"]) <= sequence <= int(row["end_sequence"])
]
if len(matches) != 1:
raise M48SmallStaticRegressionError("M4.8 assisted anchor state is ambiguous")
return matches[0]
def _compare_anchor(
anchor: dict[str, Any],
prediction_rows: dict[tuple[str, int], dict[str, Any]],
threshold: float,
) -> dict[str, Any]:
key = (str(anchor["clip_id"]), int(anchor["sequence"]))
row = prediction_rows.get(key)
if row is None or row.get("terminal_outcome") != "delivered":
raise M48SmallStaticRegressionError("M4.8 assisted anchor lacks delivered prediction row")
objects = row.get("objects")
if not isinstance(objects, list):
raise M48SmallStaticRegressionError("M4.8 prediction objects are invalid")
normalized: list[dict[str, Any]] = []
for raw in objects:
item = _object(raw, "M4.8 prediction object")
normalized.append({
"prediction_id": item.get("prediction_id"),
"extent_xyxy": _extent(item.get("extent_xyxy")),
"geometry_association": item.get("geometry_association"),
"freshness": item.get("freshness"),
"motion": item.get("motion"),
"threat": item.get("threat"),
})
ranked = sorted(
((_iou(anchor["extent_xyxy"], item["extent_xyxy"]), item) for item in normalized),
key=lambda pair: (pair[0], str(pair[1].get("prediction_id"))),
reverse=True,
)
best_iou, best = ranked[0] if ranked else (0.0, None)
return {
"schema_version": M48_SMALL_STATIC_COMPARISON_SCHEMA,
"anchor_id": anchor["anchor_id"],
"clip_id": anchor["clip_id"],
"sequence": anchor["sequence"],
"source_time_ns": row.get("source_time_ns"),
"anchor_extent_xyxy": anchor["extent_xyxy"],
"requires_avoidance_or_clearance": anchor["requires_avoidance_or_clearance"],
"worker_candidate_count": len(normalized),
"worker_objects": normalized,
"best_prediction_id": best.get("prediction_id") if best else None,
"best_iou": best_iou,
"extent_iou_threshold": threshold,
"matched_at_threshold": best_iou >= threshold,
"outcome": "recalled" if best_iou >= threshold else "missed-assisted-anchor",
}
def _publish_result(
*,
destination: Path,
identity: dict[str, Any],
created_at_utc: str,
accepted: bool,
report: dict[str, Any],
anchors: tuple[dict[str, Any], ...],
comparisons: tuple[dict[str, Any], ...],
) -> None:
parent = destination.parent
if parent.is_symlink():
raise M48SmallStaticRegressionError("M4.8 regression output root must not be a symlink")
parent.mkdir(mode=0o700, parents=True, exist_ok=True)
if not parent.is_dir():
raise M48SmallStaticRegressionError("M4.8 regression output root is invalid")
staging = parent / f".{destination.name}.{uuid.uuid4().hex}.tmp"
staging.mkdir(mode=0o700, exist_ok=False)
try:
_write_json(staging / "report.json", report)
_write_jsonl(staging / "anchors.jsonl", anchors)
_write_jsonl(staging / "comparisons.jsonl", comparisons)
artifacts = [
_artifact(
staging / "anchors.jsonl",
"assisted-regression-anchors",
M48_SMALL_STATIC_ANCHOR_SCHEMA,
),
_artifact(
staging / "comparisons.jsonl",
"exact-frame-worker-comparisons",
M48_SMALL_STATIC_COMPARISON_SCHEMA,
),
_artifact(
staging / "report.json",
"m48-small-static-regression-report",
M48_SMALL_STATIC_REPORT_SCHEMA,
),
]
manifest = {
"schema_version": M48_SMALL_STATIC_RESULT_SCHEMA,
"result_id": destination.name,
"identity_sha256": _canonical_sha256(identity),
"identity": identity,
"created_at_utc": created_at_utc,
"accepted": accepted,
"ground_truth": False,
"authority": dict(_AUTHORITY),
"artifacts": artifacts,
}
_write_json(staging / "manifest.json", manifest)
if destination.exists():
existing = {
path.name: _file_sha256(path)
for path in destination.iterdir()
if path.is_file()
}
proposed = {
path.name: _file_sha256(path)
for path in staging.iterdir()
if path.is_file()
}
if existing != proposed:
raise M48SmallStaticRegressionError("immutable M4.8 regression identity collided")
shutil.rmtree(staging)
return
os.replace(staging, destination)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
def _artifact(path: Path, role: str, schema_version: str) -> dict[str, object]:
return {
"path": path.name,
"role": role,
"byte_length": path.stat().st_size,
"sha256": _file_sha256(path),
"schema_version": schema_version,
"media_type": "application/x-ndjson" if path.suffix == ".jsonl" else "application/json",
}
def _read_json_bytes(path: Path, *, maximum: int, label: str) -> tuple[bytes, dict[str, Any]]:
candidate = path.expanduser().absolute()
if candidate.is_symlink() or not candidate.is_file() or candidate.stat().st_size > maximum:
raise M48SmallStaticRegressionError(f"{label} is unavailable")
try:
encoded = candidate.read_bytes()
value = json.loads(encoded)
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise M48SmallStaticRegressionError(f"{label} is unreadable") from exc
return encoded, _object(value, label)
def _read_json(path: Path, *, maximum: int) -> dict[str, Any]:
return _read_json_bytes(path, maximum=maximum, label=path.name)[1]
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
if path.is_symlink() or not path.is_file() or path.stat().st_size > 8 * 1024 * 1024:
raise M48SmallStaticRegressionError("M4.8 regression rows are unavailable")
rows: list[dict[str, Any]] = []
try:
with path.open("r", encoding="utf-8") as stream:
for line in stream:
if line.strip():
rows.append(_object(json.loads(line), "M4.8 regression row"))
except (OSError, json.JSONDecodeError) as exc:
raise M48SmallStaticRegressionError("M4.8 regression rows are unreadable") from exc
return rows
def _write_json(path: Path, value: object) -> None:
path.write_bytes(_canonical_json(value) + b"\n")
def _write_jsonl(path: Path, rows: tuple[dict[str, Any], ...]) -> None:
path.write_bytes(b"".join(_canonical_json(row) + b"\n" for row in rows))
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 M48SmallStaticRegressionError(f"{label} must be an object")
return value
def _integer(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def _rate(value: object) -> bool:
return (
isinstance(value, (int, float))
and not isinstance(value, bool)
and 0.0 < float(value) <= 1.0
)
def _extent(value: object) -> list[float]:
if (
not isinstance(value, list)
or len(value) != 4
or any(not isinstance(item, (int, float)) or isinstance(item, bool) for item in value)
):
raise M48SmallStaticRegressionError("M4.8 extent is invalid")
extent = [float(item) for item in value]
if not (0.0 <= extent[0] < extent[2] <= 1.0 and 0.0 <= extent[1] < extent[3] <= 1.0):
raise M48SmallStaticRegressionError("M4.8 extent is outside the camera plane")
return extent
def _iou(left: list[float], right: list[float]) -> float:
x1 = max(left[0], right[0])
y1 = max(left[1], right[1])
x2 = min(left[2], right[2])
y2 = min(left[3], right[3])
intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1)
left_area = (left[2] - left[0]) * (left[3] - left[1])
right_area = (right[2] - right[0]) * (right[3] - right[1])
union = left_area + right_area - intersection
return intersection / union if union > 0.0 else 0.0
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode("utf-8")
def _canonical_sha256(value: object) -> str:
return hashlib.sha256(_canonical_json(value)).hexdigest()
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _utc_timestamp(value: object) -> str:
if not isinstance(value, str) or not value.strip():
raise M48SmallStaticRegressionError("M4.8 run creation time is invalid")
text = value.strip()
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError as exc:
raise M48SmallStaticRegressionError("M4.8 run creation time is invalid") from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise M48SmallStaticRegressionError("M4.8 run creation time must be UTC")
return parsed.astimezone(UTC).isoformat().replace("+00:00", "Z")
__all__ = [
"M48_SMALL_STATIC_RESULT_SCHEMA",
"M48SmallStaticRegressionError",
"M48SmallStaticRegressionResult",
"build_m48_small_static_passage_regression",
"read_m48_small_static_passage_regression",
]
+6 -1
View File
@@ -5,7 +5,11 @@ from .active import (
ActiveSessionLeaseError,
recover_stale_active_session_marker,
)
from .camera_frame import RecordedCameraFrame, RecordedCameraFrameService
from .camera_frame import (
RecordedCameraFrame,
RecordedCameraFrameService,
RecordedCameraPlaybackSource,
)
from .lab_cache import publish_lab_replay_cache
from .media import (
RECORDED_MEDIA_MANIFEST_SCHEMA,
@@ -68,6 +72,7 @@ __all__ = [
"RecordedMediaArtifact",
"RecordedCameraFrame",
"RecordedCameraFrameService",
"RecordedCameraPlaybackSource",
"RECORDED_MEDIA_MANIFEST_SCHEMA",
"RecordedMediaFile",
"RecordedMediaInspector",
+211 -17
View File
@@ -5,6 +5,7 @@ import os
import struct
import subprocess
import threading
from collections import OrderedDict
from contextlib import suppress
from dataclasses import dataclass
from pathlib import Path
@@ -15,6 +16,8 @@ from .store import SessionStore
_MAX_KEYFRAME_DISTANCE = 120
_FFMPEG_TIMEOUT_SECONDS = 15.0
_DEFAULT_MAX_DECODE_LANES = 32
_DEFAULT_MAX_SOURCE_MANIFESTS = 32
@dataclass(frozen=True, slots=True)
@@ -24,6 +27,34 @@ class RecordedCameraFrame:
width: int
height: int
sha256: str
source_fragment_sha256: str | None = None
@dataclass(frozen=True, slots=True)
class RecordedCameraPlaybackSource:
"""Immutable single-epoch camera source for bounded review playback."""
session_id: str
public_source_id: str
artifact_id: str
synchronization: str
generation_sha256: str
timeline_start_seconds: float
timeline_end_seconds: float
byte_length: int
media_type: str
segment_sha256s: tuple[str, ...]
segment_start_times_ns: tuple[int, ...]
@property
def segment_count(self) -> int:
return len(self.segment_sha256s)
@dataclass(slots=True)
class _CameraDecodeLane:
active: bool = False
latest_ticket: int = 0
class RecordedCameraFrameService:
@@ -42,6 +73,8 @@ class RecordedCameraFrameService:
*,
ffmpeg_path: Path,
cache_root: Path,
max_decode_lanes: int = _DEFAULT_MAX_DECODE_LANES,
max_source_manifests: int = _DEFAULT_MAX_SOURCE_MANIFESTS,
) -> None:
resolved_ffmpeg = ffmpeg_path.expanduser().resolve(strict=True)
if not resolved_ffmpeg.is_file() or not os.access(resolved_ffmpeg, os.X_OK):
@@ -53,7 +86,16 @@ class RecordedCameraFrameService:
self._cache_root.mkdir(parents=True, exist_ok=True)
if self._cache_root.is_symlink() or not self._cache_root.is_dir():
raise SessionIntegrityError("camera frame cache root is invalid")
self._lock = threading.Lock()
if max_decode_lanes < 1 or max_source_manifests < 1:
raise SessionIntegrityError("camera frame memory cache bounds are invalid")
self._max_decode_lanes = max_decode_lanes
self._max_source_manifests = max_source_manifests
self._coordination = threading.Condition(threading.Lock())
self._lanes: OrderedDict[tuple[str, str], _CameraDecodeLane] = OrderedDict()
self._source_manifests: OrderedDict[
tuple[str, str],
RecordedMediaManifest,
] = OrderedDict()
def extract(
self,
@@ -64,6 +106,108 @@ class RecordedCameraFrameService:
) -> RecordedCameraFrame:
if frame_index < 0:
raise SessionIntegrityError("camera frame index is invalid")
source_key = (session_id, expected_source_name)
lane, ticket = self._acquire_lane(source_key)
try:
manifest = self._source_manifest(
source_key,
session_id=session_id,
expected_source_name=expected_source_name,
)
self._require_latest(lane, ticket)
epoch, sequence = _frame_location(manifest, frame_index)
cache_key = hashlib.sha256(
(
f"{manifest.generation_sha256}\0{manifest.artifact_id}\0"
f"{expected_source_name}\0{frame_index}\0jpeg-q2-v1"
).encode()
).hexdigest()
cache_path = self._cache_root / f"{cache_key}.jpg"
cached = _read_cached_jpeg(
cache_path,
source_fragment_sha256=epoch.segments[sequence - 1].sha256,
)
if cached is not None:
self._require_latest(lane, ticket)
return cached
self._require_latest(lane, ticket)
frame = self._decode(manifest, epoch, sequence)
_publish_cached_jpeg(cache_path, frame.payload)
self._require_latest(lane, ticket)
return frame
finally:
self._release_lane(source_key, lane)
def playback_source(
self,
session_id: str,
*,
expected_source_name: str = "sensor.camera.right",
) -> RecordedCameraPlaybackSource:
"""Return the generation-bound fMP4 source used by the shared LAB viewer.
LAB playback is intentionally admitted from the same cached immutable
manifest as exact JPEG extraction. The contract is limited to one
codec epoch because LAB frame sequence is a direct one-based segment
sequence; a future multi-epoch source must add an explicit mapping
contract instead of guessing across epoch boundaries.
"""
manifest = self._source_manifest(
(session_id, expected_source_name),
session_id=session_id,
expected_source_name=expected_source_name,
)
if manifest.synchronization != "host-arrival-best-effort" or len(manifest.epochs) != 1:
raise SessionIntegrityError("recorded camera playback source is incompatible")
epoch = manifest.epochs[0]
if (
epoch.ordinal != 1
or epoch.timeline_start_seconds != manifest.timeline_start_seconds
or epoch.timeline_end_seconds != manifest.timeline_end_seconds
or not epoch.media_type.startswith("video/mp4;")
or not epoch.segments
or tuple(segment.sequence for segment in epoch.segments)
!= tuple(range(1, len(epoch.segments) + 1))
):
raise SessionIntegrityError("recorded camera playback epoch is incompatible")
starts_ns: list[int] = []
previous_end_seconds = 0.0
for segment in epoch.segments:
starts_ns.append(
round((epoch.timeline_start_seconds + previous_end_seconds) * 1_000_000_000)
)
previous_end_seconds = segment.end_time_seconds
return RecordedCameraPlaybackSource(
session_id=manifest.session_id,
public_source_id=manifest.public_source_id,
artifact_id=manifest.artifact_id,
synchronization=manifest.synchronization,
generation_sha256=manifest.generation_sha256,
timeline_start_seconds=manifest.timeline_start_seconds,
timeline_end_seconds=manifest.timeline_end_seconds,
byte_length=manifest.byte_length,
media_type=epoch.media_type,
segment_sha256s=tuple(segment.sha256 for segment in epoch.segments),
segment_start_times_ns=tuple(starts_ns),
)
def _source_manifest(
self,
source_key: tuple[str, str],
*,
session_id: str,
expected_source_name: str,
) -> RecordedMediaManifest:
"""Bind one immutable recorded source without rescanning it per frame."""
with self._coordination:
cached = self._source_manifests.get(source_key)
if cached is not None:
self._source_manifests.move_to_end(source_key)
if cached is not None:
return cached
replay = self._store.prepare_replay(session_id, speed=1.0, loop=False)
matches = tuple(
artifact
@@ -74,22 +218,66 @@ class RecordedCameraFrameService:
raise SessionIntegrityError("recorded camera source is unavailable")
artifact = matches[0]
manifest = self._inspector.inspect(artifact, replay)
epoch, sequence = _frame_location(manifest, frame_index)
cache_key = hashlib.sha256(
(
f"{manifest.generation_sha256}\0{artifact.artifact_id}\0"
f"{expected_source_name}\0{frame_index}\0jpeg-q2-v1"
).encode()
).hexdigest()
cache_path = self._cache_root / f"{cache_key}.jpg"
with self._coordination:
bound = self._source_manifests.get(source_key)
if bound is None:
bound = manifest
self._source_manifests[source_key] = manifest
self._source_manifests.move_to_end(source_key)
while len(self._source_manifests) > self._max_source_manifests:
self._source_manifests.popitem(last=False)
return bound
with self._lock:
cached = _read_cached_jpeg(cache_path)
if cached is not None:
return cached
frame = self._decode(manifest, epoch, sequence)
_publish_cached_jpeg(cache_path, frame.payload)
return frame
def _acquire_lane(
self,
source_key: tuple[str, str],
) -> tuple[_CameraDecodeLane, int]:
"""Admit only the newest waiter behind one active source decode."""
with self._coordination:
lane = self._lanes.get(source_key)
if lane is None:
lane = _CameraDecodeLane()
self._lanes[source_key] = lane
self._lanes.move_to_end(source_key)
self._evict_inactive_lanes(exclude=source_key)
lane.latest_ticket += 1
ticket = lane.latest_ticket
self._coordination.notify_all()
while lane.active:
if ticket != lane.latest_ticket:
raise SessionIntegrityError("camera frame request was superseded")
self._coordination.wait()
if ticket != lane.latest_ticket:
raise SessionIntegrityError("camera frame request was superseded")
lane.active = True
return lane, ticket
def _evict_inactive_lanes(self, *, exclude: tuple[str, str] | None = None) -> None:
if len(self._lanes) <= self._max_decode_lanes:
return
for source_key, lane in tuple(self._lanes.items()):
if len(self._lanes) <= self._max_decode_lanes:
break
if source_key != exclude and not lane.active:
del self._lanes[source_key]
def _require_latest(self, lane: _CameraDecodeLane, ticket: int) -> None:
with self._coordination:
if ticket != lane.latest_ticket:
raise SessionIntegrityError("camera frame request was superseded")
def _release_lane(
self,
source_key: tuple[str, str],
lane: _CameraDecodeLane,
) -> None:
with self._coordination:
lane.active = False
if self._lanes.get(source_key) is lane:
self._lanes.move_to_end(source_key)
self._evict_inactive_lanes()
self._coordination.notify_all()
def _decode(
self,
@@ -156,6 +344,7 @@ class RecordedCameraFrameService:
width=width,
height=height,
sha256=digest,
source_fragment_sha256=target.sha256,
)
@@ -196,7 +385,11 @@ def _jpeg_dimensions(payload: bytes) -> tuple[int, int]:
raise SessionIntegrityError("camera frame JPEG dimensions are unavailable")
def _read_cached_jpeg(path: Path) -> RecordedCameraFrame | None:
def _read_cached_jpeg(
path: Path,
*,
source_fragment_sha256: str,
) -> RecordedCameraFrame | None:
try:
if path.is_symlink() or not path.is_file():
return None
@@ -210,6 +403,7 @@ def _read_cached_jpeg(path: Path) -> RecordedCameraFrame | None:
width=width,
height=height,
sha256=hashlib.sha256(payload).hexdigest(),
source_fragment_sha256=source_fragment_sha256,
)
+11 -5
View File
@@ -56,6 +56,7 @@ from k1link.compute.e40_perception_product_gate import (
read_e40_perception_product_gate,
)
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
from k1link.laboratory.evidence_registry import LaboratoryEvidenceVariant
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
from k1link.web.l31_pointpillars_ravnoves_api import latest_l31_identity
from k1link.web.l32_pointpillars_camera_review_api import latest_l32_identity
@@ -259,7 +260,10 @@ def _advanced_index(
specs: tuple[_AdvancedIndexSpec, ...],
) -> dict[str, object]:
items: list[dict[str, object]] = []
selected_work_ids: set[str] = set()
for work_id, provider, pattern, document_name, schema_version in specs:
if work_id in selected_work_ids:
continue
root = _configured_root(provider)
if root is None:
continue
@@ -273,6 +277,7 @@ def _advanced_index(
schema_version=schema_version,
)
)
selected_work_ids.add(work_id)
break
except (json.JSONDecodeError, OSError, TypeError, ValueError):
continue
@@ -290,17 +295,18 @@ def _registry_index_specs(
return tuple(
(
definition.work_id,
_evidence_root_provider(definition, runtime_root_provider),
definition.result_id_pattern,
definition.document_name,
definition.result_schema_version,
_evidence_root_provider(variant, runtime_root_provider),
variant.result_id_pattern,
variant.document_name,
variant.result_schema_version,
)
for definition in registry.definitions
for variant in reversed(definition.evidence_variants)
)
def _evidence_root_provider(
definition: LaboratoryEvidenceDefinition,
definition: LaboratoryEvidenceDefinition | LaboratoryEvidenceVariant,
runtime_root_provider: RootProvider,
) -> RootProvider:
def result_root_provider() -> Path | None:
+85
View File
@@ -23,15 +23,23 @@ from k1link.compute import (
RecordedPerceptionOverlayMux,
RecordedPerceptionOverlayStore,
)
from k1link.compute.pipeline_telemetry import JsonlPipelineTelemetrySink
from k1link.laboratory import (
LaboratoryEvidenceRegistry,
LaboratoryEvidenceReportService,
LaboratoryExecutionRegistry,
LaboratoryRunner,
LaboratoryValueReviewRegistry,
)
from k1link.laboratory.m48_raw_evidence import (
M48_EXPECTED_THREAT_RESULT_ID,
M48RawEvidenceError,
M48RawEvidenceReader,
)
from k1link.sessions import (
MaterializedRecording,
RecordedCameraFrameService,
RecordedCameraPlaybackSource,
RecordedMediaInspector,
RecordedMediaManifest,
RecordingPreparationQueueFull,
@@ -116,6 +124,7 @@ from k1link.web.laboratory_report_api import build_laboratory_report_router
from k1link.web.lidar_api import build_lidar_router
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
from k1link.web.m48_object_quality_api import build_m48_object_quality_router
from k1link.web.map_api import (
MapGatewayConfiguration,
MapGatewayProxy,
@@ -151,6 +160,13 @@ LABORATORY_EXECUTION_REGISTRY = LaboratoryExecutionRegistry.from_file(
REPOSITORY_ROOT / "config" / "laboratory-execution.json",
LABORATORY_EVIDENCE_REGISTRY,
)
LABORATORY_RUNNER = LaboratoryRunner(
registry=LABORATORY_EXECUTION_REGISTRY,
evidence_registry=LABORATORY_EVIDENCE_REGISTRY,
sink=JsonlPipelineTelemetrySink(
REPOSITORY_ROOT / ".runtime" / "telemetry" / "laboratory-runs.jsonl"
),
)
LABORATORY_VALUE_REVIEW_REGISTRY = LaboratoryValueReviewRegistry.from_file(
REPOSITORY_ROOT / "config" / "laboratory-value-review.json"
)
@@ -204,6 +220,20 @@ session_recorded_camera_frame_service = (
if _ffmpeg is not None
else None
)
try:
m48_raw_evidence_reader: M48RawEvidenceReader | None = M48RawEvidenceReader.from_repository(
repository_root=REPOSITORY_ROOT,
threat_result_root=(
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m4"
/ "replay-threat"
/ M48_EXPECTED_THREAT_RESULT_ID
),
)
except (M48RawEvidenceError, OSError, ValueError):
m48_raw_evidence_reader = None
session_legacy_perception_overlay_store = (
RecordedPerceptionOverlayStore(
jobs_root=REPOSITORY_ROOT / ".runtime" / "compute-jobs",
@@ -295,6 +325,20 @@ session_recording_preparation_manager = SessionRecordingPreparationManager(
)
def _m48_recorded_camera_playback_source(
session_id: str,
) -> RecordedCameraPlaybackSource:
"""Publish the durable replay package before exposing its manifest URL."""
if session_recorded_camera_frame_service is None:
raise RuntimeError("recorded camera playback is unavailable")
command = session_store.prepare_replay(session_id, speed=1.0, loop=False)
snapshot = session_recording_preparation_manager.restore_published(command)
if snapshot is None or snapshot.state != "ready" or snapshot.recorded_media is None:
raise RuntimeError("recorded camera playback package is not published")
return session_recorded_camera_frame_service.playback_source(session_id)
def refresh_observation_catalog() -> tuple[str, ...]:
"""Discover completed or recoverable local evidence without copying payloads."""
@@ -859,6 +903,47 @@ app.include_router(
),
)
)
app.include_router(
build_m48_object_quality_router(
pack_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m48" / "object-quality-packs"
),
workflow_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "laboratory-annotations" / "m48-object-quality"
),
truth_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m48" / "object-truth-seals"
),
result_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "m48" / "object-quality-results"
),
small_static_result_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m48"
/ "small-static-passage-regression-results"
),
camera_frame_provider=(
session_recorded_camera_frame_service.extract
if session_recorded_camera_frame_service is not None
else None
),
camera_playback_provider=(
_m48_recorded_camera_playback_source
if session_recorded_camera_frame_service is not None
else None
),
spatial_evidence_provider=m48_raw_evidence_reader,
evaluation_runner=LABORATORY_RUNNER,
evaluation_receipt_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "laboratory-run-receipts"
),
)
)
app.include_router(
build_e47_semantic_slam_router(
root_provider=lambda: (
File diff suppressed because it is too large Load Diff
+78 -1
View File
@@ -9,7 +9,11 @@ from fastapi.routing import APIRoute
from pytest import MonkeyPatch
import k1link.web.advanced_laboratory_api as advanced_api
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
from k1link.laboratory import (
LaboratoryEvidenceDefinition,
LaboratoryEvidenceRegistry,
LaboratoryEvidenceVariant,
)
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
@@ -74,6 +78,79 @@ def test_advanced_index_is_empty_when_not_configured() -> None:
}
def test_advanced_index_projects_one_most_mature_lifecycle_phase(tmp_path: Path) -> None:
variants = (
LaboratoryEvidenceVariant(
phase="review",
runtime_relative_root=PurePosixPath("packs"),
result_id_prefix="quality-pack",
document_name="manifest.json",
result_schema_version="missioncore.quality-pack/v1",
),
LaboratoryEvidenceVariant(
phase="result",
runtime_relative_root=PurePosixPath("results"),
result_id_prefix="quality-result",
document_name="manifest.json",
result_schema_version="missioncore.quality-result/v1",
),
)
registry = LaboratoryEvidenceRegistry(
definitions=(
LaboratoryEvidenceDefinition(
work_id="quality-lab",
runtime_relative_root=variants[-1].runtime_relative_root,
result_id_prefix=variants[-1].result_id_prefix,
document_name=variants[-1].document_name,
result_schema_version=variants[-1].result_schema_version,
lifecycle_variants=variants,
),
)
)
def publish(variant: LaboratoryEvidenceVariant, digest: str, created_at: str) -> str:
result_id = f"{variant.result_id_prefix}-{digest}"
result_root = variant.result_root(tmp_path) / result_id
result_root.mkdir(parents=True)
(result_root / variant.document_name).write_text(
json.dumps(
{
"schema_version": variant.result_schema_version,
"result_id": result_id,
"identity_sha256": digest,
"identity": {
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
},
"created_at_utc": created_at,
}
),
encoding="utf-8",
)
return result_id
pack_id = publish(variants[0], "a" * 64, "2026-08-24T10:00:00Z")
router = build_advanced_laboratory_router(
evidence_registry=registry,
evidence_runtime_root_provider=lambda: tmp_path,
)
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
assert route()["items"][0]["result_id"] == pack_id # type: ignore[index,operator]
result_id = publish(variants[1], "b" * 64, "2026-08-24T11:00:00Z")
index = route() # type: ignore[operator]
assert index["items"] == [ # type: ignore[index]
{
"work_id": "quality-lab",
"result_id": result_id,
"created_at_utc": "2026-08-24T11:00:00Z",
"access": "read-only",
}
]
def test_advanced_index_includes_valid_l31_identity(
tmp_path: Path,
monkeypatch: MonkeyPatch,
+327
View File
@@ -0,0 +1,327 @@
from __future__ import annotations
import hashlib
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import pytest
from k1link.sessions.camera_frame import RecordedCameraFrame, RecordedCameraFrameService
from k1link.sessions.media import (
RecordedMediaEpoch,
RecordedMediaManifest,
RecordedMediaSegment,
)
from k1link.sessions.models import RecordedMediaArtifact, SessionIntegrityError
_JPEG = b"\xff\xd8\xff\xc0\x00\x07\x08\x00\x01\x00\x01\xff\xd9"
class _Store:
def __init__(self, *, block_prepare: bool = False) -> None:
self.prepare_calls = 0
self.list_calls = 0
self.prepare_started = threading.Event()
self.release_prepare = threading.Event()
if not block_prepare:
self.release_prepare.set()
self.artifact = RecordedMediaArtifact(
session_id="recorded-session",
public_source_id="camera",
artifact_id="camera-artifact",
source_path=Path("/sealed/sensor.camera.right"),
byte_length=123,
)
def prepare_replay(self, session_id: str, **_: Any) -> SimpleNamespace:
self.prepare_calls += 1
self.prepare_started.set()
assert self.release_prepare.wait(timeout=2.0)
return SimpleNamespace(session_id=session_id)
def list_recorded_media(self, session_id: str) -> tuple[RecordedMediaArtifact, ...]:
self.list_calls += 1
return (self.artifact,)
class _Inspector:
def __init__(self, manifest: RecordedMediaManifest) -> None:
self.manifest = manifest
self.inspect_calls = 0
def inspect(self, artifact: RecordedMediaArtifact, replay: object) -> RecordedMediaManifest:
assert artifact.artifact_id == "camera-artifact"
assert replay is not None
self.inspect_calls += 1
return self.manifest
def _manifest(tmp_path: Path) -> RecordedMediaManifest:
epoch_path = tmp_path / "epoch-1"
segments = tuple(
RecordedMediaSegment(
sequence=sequence,
path=epoch_path / "segments" / f"{sequence:08d}.m4s",
byte_length=10,
sha256=hashlib.sha256(str(sequence).encode()).hexdigest(),
random_access=True,
end_time_seconds=float(sequence),
)
for sequence in range(1, 4)
)
return RecordedMediaManifest(
session_id="recorded-session",
public_source_id="camera",
artifact_id="camera-artifact",
synchronization="recorded",
generation_sha256="a" * 64,
timeline_start_seconds=0.0,
timeline_end_seconds=3.0,
byte_length=123,
epochs=(
RecordedMediaEpoch(
ordinal=1,
path=epoch_path,
init_path=epoch_path / "init.mp4",
init_byte_length=10,
init_sha256="b" * 64,
media_type="video/mp4",
timeline_start_seconds=0.0,
timeline_end_seconds=3.0,
segments=segments,
),
),
)
def _service(
tmp_path: Path,
store: _Store,
inspector: _Inspector,
*,
max_decode_lanes: int = 32,
max_source_manifests: int = 32,
) -> RecordedCameraFrameService:
return RecordedCameraFrameService(
store, # type: ignore[arg-type]
inspector, # type: ignore[arg-type]
ffmpeg_path=Path(sys.executable),
cache_root=tmp_path / "cache",
max_decode_lanes=max_decode_lanes,
max_source_manifests=max_source_manifests,
)
def _wait_for_latest(service: RecordedCameraFrameService, ticket: int) -> None:
deadline = time.monotonic() + 2.0
source_key = ("recorded-session", "sensor.camera.right")
while time.monotonic() < deadline:
with service._coordination: # noqa: SLF001
lane = service._lanes.get(source_key) # noqa: SLF001
if lane is not None and lane.latest_ticket == ticket:
return
time.sleep(0.005)
raise AssertionError(f"camera request ticket {ticket} was not registered")
def _frame(epoch: RecordedMediaEpoch, sequence: int) -> RecordedCameraFrame:
return RecordedCameraFrame(
payload=_JPEG,
media_type="image/jpeg",
width=1,
height=1,
sha256=hashlib.sha256(_JPEG).hexdigest(),
source_fragment_sha256=epoch.segments[sequence - 1].sha256,
)
def test_camera_frame_burst_scans_manifest_once_and_decodes_only_latest(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store = _Store(block_prepare=True)
inspector = _Inspector(_manifest(tmp_path))
service = _service(tmp_path, store, inspector)
decoded: list[int] = []
def decode(
manifest: RecordedMediaManifest,
epoch: RecordedMediaEpoch,
sequence: int,
) -> RecordedCameraFrame:
assert manifest is inspector.manifest
decoded.append(sequence)
return _frame(epoch, sequence)
monkeypatch.setattr(service, "_decode", decode)
with ThreadPoolExecutor(max_workers=3) as pool:
first = pool.submit(service.extract, "recorded-session", 0)
assert store.prepare_started.wait(timeout=2.0)
second = pool.submit(service.extract, "recorded-session", 1)
_wait_for_latest(service, 2)
third = pool.submit(service.extract, "recorded-session", 2)
_wait_for_latest(service, 3)
store.release_prepare.set()
with pytest.raises(SessionIntegrityError, match="superseded"):
first.result(timeout=2.0)
with pytest.raises(SessionIntegrityError, match="superseded"):
second.result(timeout=2.0)
assert third.result(timeout=2.0).source_fragment_sha256 == (
inspector.manifest.epochs[0].segments[2].sha256
)
assert store.prepare_calls == 1
assert store.list_calls == 1
assert inspector.inspect_calls == 1
assert decoded == [3]
def test_camera_frame_lane_keeps_only_latest_waiter_behind_active_decode(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store = _Store()
inspector = _Inspector(_manifest(tmp_path))
service = _service(tmp_path, store, inspector)
decode_started = threading.Event()
release_decode = threading.Event()
decoded: list[int] = []
def decode(
manifest: RecordedMediaManifest,
epoch: RecordedMediaEpoch,
sequence: int,
) -> RecordedCameraFrame:
assert manifest is inspector.manifest
decoded.append(sequence)
if sequence == 1:
decode_started.set()
assert release_decode.wait(timeout=2.0)
return _frame(epoch, sequence)
monkeypatch.setattr(service, "_decode", decode)
with ThreadPoolExecutor(max_workers=3) as pool:
first = pool.submit(service.extract, "recorded-session", 0)
assert decode_started.wait(timeout=2.0)
second = pool.submit(service.extract, "recorded-session", 1)
_wait_for_latest(service, 2)
third = pool.submit(service.extract, "recorded-session", 2)
_wait_for_latest(service, 3)
release_decode.set()
with pytest.raises(SessionIntegrityError, match="superseded"):
first.result(timeout=2.0)
with pytest.raises(SessionIntegrityError, match="superseded"):
second.result(timeout=2.0)
assert third.result(timeout=2.0).width == 1
assert decoded == [1, 3]
assert store.prepare_calls == 1
assert inspector.inspect_calls == 1
def test_camera_frame_memory_caches_are_lru_bounded(tmp_path: Path) -> None:
store = _Store()
inspector = _Inspector(_manifest(tmp_path))
service = _service(
tmp_path,
store,
inspector,
max_decode_lanes=2,
max_source_manifests=2,
)
for ordinal in range(4):
source_key = (f"session-{ordinal}", "sensor.camera.right")
lane, _ = service._acquire_lane(source_key) # noqa: SLF001
service._release_lane(source_key, lane) # noqa: SLF001
assert tuple(service._lanes) == ( # noqa: SLF001
("session-2", "sensor.camera.right"),
("session-3", "sensor.camera.right"),
)
for ordinal in range(3):
source_key = (f"session-{ordinal}", "sensor.camera.right")
service._source_manifest( # noqa: SLF001
source_key,
session_id=source_key[0],
expected_source_name=source_key[1],
)
assert tuple(service._source_manifests) == ( # noqa: SLF001
("session-1", "sensor.camera.right"),
("session-2", "sensor.camera.right"),
)
service._source_manifest( # noqa: SLF001
("session-1", "sensor.camera.right"),
session_id="session-1",
expected_source_name="sensor.camera.right",
)
service._source_manifest( # noqa: SLF001
("session-3", "sensor.camera.right"),
session_id="session-3",
expected_source_name="sensor.camera.right",
)
assert tuple(service._source_manifests) == ( # noqa: SLF001
("session-1", "sensor.camera.right"),
("session-3", "sensor.camera.right"),
)
assert store.prepare_calls == 4
assert inspector.inspect_calls == 4
def test_camera_playback_source_reuses_manifest_and_exposes_exact_segment_clock(
tmp_path: Path,
) -> None:
store = _Store()
manifest = _manifest(tmp_path)
manifest = RecordedMediaManifest(
session_id=manifest.session_id,
public_source_id=manifest.public_source_id,
artifact_id=manifest.artifact_id,
synchronization="host-arrival-best-effort",
generation_sha256=manifest.generation_sha256,
timeline_start_seconds=manifest.timeline_start_seconds,
timeline_end_seconds=manifest.timeline_end_seconds,
byte_length=manifest.byte_length,
epochs=(
RecordedMediaEpoch(
ordinal=1,
path=manifest.epochs[0].path,
init_path=manifest.epochs[0].init_path,
init_byte_length=manifest.epochs[0].init_byte_length,
init_sha256=manifest.epochs[0].init_sha256,
media_type='video/mp4; codecs="avc1.640028"',
timeline_start_seconds=0.0,
timeline_end_seconds=3.0,
segments=manifest.epochs[0].segments,
),
),
)
inspector = _Inspector(manifest)
service = _service(tmp_path, store, inspector)
first = service.playback_source("recorded-session")
second = service.playback_source("recorded-session")
assert first == second
assert first.segment_count == 3
assert first.segment_sha256s == tuple(
segment.sha256 for segment in manifest.epochs[0].segments
)
assert first.segment_start_times_ns == (0, 1_000_000_000, 2_000_000_000)
assert store.prepare_calls == 1
assert inspector.inspect_calls == 1
def test_camera_frame_rejects_unbounded_memory_cache_configuration(tmp_path: Path) -> None:
store = _Store()
inspector = _Inspector(_manifest(tmp_path))
with pytest.raises(SessionIntegrityError, match="cache bounds"):
_service(tmp_path, store, inspector, max_decode_lanes=0)
+12 -1
View File
@@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
repository_root / "config" / "laboratories"
)
assert len(registry.definitions) == 34
assert len(registry.definitions) == 36
assert {item.work_id for item in registry.definitions} >= {
"e31-source-binding",
"e46j-raw-fisheye-realtime",
@@ -139,4 +139,15 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
"l34f-adjudicated-reference",
"m4-replay-threat",
"m47-reference-graph-shadow",
"m48-object-centric-quality",
"m48-small-static-passage-regression",
}
m48 = next(
item for item in registry.definitions
if item.work_id == "m48-object-centric-quality"
)
assert [variant.phase for variant in m48.evidence_variants] == ["review", "result"]
assert [variant.result_id_prefix for variant in m48.evidence_variants] == [
"m48-object-quality-pack",
"m48-object-quality-result",
]
+68
View File
@@ -4,6 +4,7 @@ import hashlib
import json
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
import pytest
@@ -90,6 +91,8 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
evidence, execution = _registries()
assert {row.work_id for row in execution.definitions} == {
"m48-small-static-passage-regression",
"m48-object-centric-quality",
"m4-replay-threat",
"e33-worker-shadow",
"e35-degradation-recovery",
@@ -97,6 +100,12 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
"e47-semantic-slam-shadow",
}
by_work_id = {row.work_id: row for row in execution.definitions}
assert by_work_id["m48-small-static-passage-regression"].evidence_contract == (
"missioncore.m48-small-static-passage-regression-result/v1"
)
assert by_work_id["m48-object-centric-quality"].evidence_contract == (
"missioncore.m48-object-centric-quality-result/v1"
)
assert by_work_id["e47-semantic-slam-shadow"].lifecycle == "experimental"
assert by_work_id["e47-semantic-slam-shadow"].isolation == "bounded-adapter"
assert all(
@@ -193,6 +202,65 @@ def test_runner_rejects_undeclared_input_before_adapter(tmp_path: Path) -> None:
assert called is False
def test_m48_evaluation_uses_registered_adapter_and_common_receipt(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
evidence, execution = _registries()
pack_root = tmp_path / "pack"
truth_root = tmp_path / "truth"
pack_root.mkdir()
truth_root.mkdir()
adapter_result = _evidence_result(
tmp_path / "results",
work_id="m48-object-centric-quality",
)
def score(**kwargs: Path) -> SimpleNamespace:
assert kwargs == {
"pack_root": pack_root,
"truth_seal_root": truth_root,
"output_root": tmp_path / "results",
}
return SimpleNamespace(
result_root=adapter_result.result_root,
result_id=adapter_result.result_id,
)
monkeypatch.setattr(
"k1link.laboratory.m48_object_quality.score_m48_object_quality",
score,
)
runner = LaboratoryRunner(
registry=execution,
evidence_registry=evidence,
sink=JsonlPipelineTelemetrySink(tmp_path / "pipeline.jsonl"),
)
request = LaboratoryRunRequest(
work_id="m48-object-centric-quality",
run_id="m48-evaluation-fixture",
request_id="evaluate-once",
contour_id="mission-core-lab",
agent_id="local-control-plane",
node_id="fixture-node",
source_id="m48-pack-fixture",
source_package_id="m48-truth-fixture",
method_id="m48-object-centric-quality/v1",
inputs={"pack_root": pack_root, "truth_seal_root": truth_root},
output_root=tmp_path / "results",
receipt_root=tmp_path / "receipts",
)
result = runner.run(request)
assert result.result_id == adapter_result.result_id
assert result.receipt["adapter_id"] == "canonical.m48-object-centric-quality/v1"
assert result.receipt["contracts"]["evidence"] == (
"missioncore.m48-object-centric-quality-result/v1"
)
assert (result.receipt_root / "receipt.json").is_file()
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
+613
View File
@@ -0,0 +1,613 @@
from __future__ import annotations
import copy
import hashlib
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import pytest
import k1link.laboratory.m48_object_quality as m48
from k1link.laboratory.evidence_registry import LaboratoryEvidenceRegistry
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
def _write_json(path: Path, value: object) -> None:
path.write_text(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n")
def _frame_catalog() -> list[dict[str, object]]:
return [
{
"sequence": sequence,
"source_time_ns": (sequence - 1) * 100_000_000,
"camera_fragment_sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(),
}
for sequence in range(1, 4490)
]
def _clips() -> list[dict[str, object]]:
rows = []
for index in range(20):
start = 1 + index * 100
split = "development" if index < 10 else "validation"
split_index = index if index < 10 else index - 10
rows.append(
{
"clip_id": f"clip-{index:02d}",
"component_id": f"component-{split}-{split_index // 2:02d}",
"route_block": f"route-{split}-{split_index // 3:02d}",
"time_block": f"time-{split}-{split_index // 2:02d}",
"split": split,
"start_sequence": start,
"end_sequence": start + 50,
}
)
return rows
def _clip_fixture_state(clip_id: str) -> dict[str, object]:
local_index = int(clip_id.rsplit("-", 1)[1]) % 10
if local_index == 0:
return {
"extent_xyxy": [0.2, 0.2, 0.22, 0.22],
"geometry_association": "unknown",
"motion": "unsupported",
"threat": "unknown",
"unknown_causes": [
"insufficient-geometry-support",
"threat-evidence-insufficient",
],
}
if local_index == 1:
return {
"extent_xyxy": [0.01, 0.2, 0.2, 0.4],
"geometry_association": "associated",
"motion": "static",
"threat": "not-threat",
"unknown_causes": [],
}
if local_index == 2:
return {
"extent_xyxy": [0.1, 0.1, 0.3, 0.4],
"geometry_association": "associated",
"motion": "moving",
"threat": "threat",
"unknown_causes": [],
}
return {
"extent_xyxy": [0.1, 0.1, 0.3, 0.4],
"geometry_association": "associated",
"motion": "static",
"threat": "not-threat",
"unknown_causes": [],
}
def _preparation_provenance() -> dict[str, object]:
return {
"schema_version": m48.M48_PREPARATION_PROVENANCE_SCHEMA,
"adapter": {
"module": "k1link.laboratory.m48_ravnoves00_pack",
"sha256": "1" * 64,
},
"selection": {
"selection_id": "m48-ravnoves00-balanced-connected-clips/v1",
"sha256": "2" * 64,
},
"camera_index": {
"source_session_id": "20260720T065719Z_viewer_live",
"sha256": "3" * 64,
"byte_length": 1234,
"frame_count": 4489,
},
"graph": {
"result_id": "m47-reference-graph-" + "4" * 64,
"manifest_sha256": "5" * 64,
"frames_sha256": "6" * 64,
},
"threat": {
"result_id": "m4-threat-replay-" + "7" * 64,
"manifest_sha256": "8" * 64,
"frames_sha256": "9" * 64,
},
"geometry": {
"result_id": "m4-geometry-replay-" + "a" * 64,
"manifest_sha256": "b" * 64,
"frames_sha256": "c" * 64,
},
}
def _prediction_rows(
clips: list[dict[str, object]],
*,
unsafe_free_space: bool,
unsafe_free_space_split: str | None = None,
) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
for clip in clips:
fixture = _clip_fixture_state(str(clip["clip_id"]))
unsafe = unsafe_free_space and (
unsafe_free_space_split is None or clip["split"] == unsafe_free_space_split
)
for sequence in range(int(clip["start_sequence"]), int(clip["end_sequence"]) + 1):
objects = [
{
"prediction_id": f"prediction-{sequence}",
"extent_xyxy": fixture["extent_xyxy"],
"geometry_association": fixture["geometry_association"],
"freshness": "current",
"motion": fixture["motion"],
"threat": fixture["threat"],
"unknown_causes": fixture["unknown_causes"],
}
]
rows.append(
{
"sequence": sequence,
"source_time_ns": (sequence - 1) * 100_000_000,
"terminal_outcome": "delivered",
"terminal_reason": None,
"free_space_claimed": unsafe,
"objects": objects,
}
)
return rows
def _fake_m47(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / f"m47-reference-graph-lab-{'a' * 64}"
root.mkdir()
manifest = {
"schema_version": "missioncore.reference-perception-graph-lab/v2",
"accepted": True,
"ground_truth": False,
}
_write_json(root / "manifest.json", manifest)
report = {
"source": {
"source_id": "RAVNOVES00",
"source_session_id": "20260720T065719Z_viewer_live",
"graph_result_id": "m47-reference-graph-" + "b" * 64,
},
"method": {
"graph_id": "reference-perception-graph/v2",
"run_mode": "lossless-replay",
"canonical_payload_sha256": "c" * 64,
},
"decision": {
"state": "accepted-reference-graph-replay",
"next_gate": "independent-object-centric-detection-quality",
},
"acceptance": {"accepted": True},
"authority": {
"mode": "replay-simulated",
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
"ground_truth": False,
},
}
monkeypatch.setattr(
m48,
"read_m47_reference_graph_lab",
lambda _: SimpleNamespace(
result_id=root.name,
result_root=root,
manifest=manifest,
report=report,
),
)
def _review_clips(clips: tuple[dict[str, Any], ...], *, state: str) -> list[dict[str, Any]]:
rows = []
for clip in clips:
fixture = _clip_fixture_state(str(clip["clip_id"]))
start = int(clip["start_sequence"])
end = int(clip["end_sequence"])
rows.append(
{
"clip_id": clip["clip_id"],
"start_sequence": start,
"end_sequence": end,
"review_state": state,
"no_object": False,
"tracklets": [
{
"object_id": "object-1",
"first_sequence": start,
"last_sequence": end,
"keyframes": [
{
"sequence": start,
"extent_xyxy": fixture["extent_xyxy"],
"visibility": "visible",
},
{
"sequence": end,
"extent_xyxy": fixture["extent_xyxy"],
"visibility": "partial",
},
],
"state_segments": [
{
"start_sequence": start,
"end_sequence": end,
"geometry_association": fixture["geometry_association"],
"freshness": "current",
"motion": fixture["motion"],
"threat": fixture["threat"],
"critical_corridor_obstacle": True,
}
],
"notes": None,
}
],
"notes": None,
}
)
return rows
def _review(pack: m48.M48ObjectQualityPack, reviewer_id: str) -> dict[str, Any]:
return {
"schema_version": m48.M48_REVIEW_SCHEMA,
"pack_id": pack.result_id,
"state": "completed-independent-no-predictions",
"reviewer_id": reviewer_id,
"review_round": 1,
"blindness": {
"candidate_identity_seen": False,
"model_predictions_seen": False,
"model_scores_seen": False,
"semantic_class_task_seen": False,
},
"clips": _review_clips(pack.clips, state="reviewed"),
"acceptance": {
"all_clips_reviewed": True,
"independent": True,
"submitted_at_utc": "2026-08-24T11:00:00Z",
},
}
def _build_generations(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
*,
unsafe_free_space: bool = False,
unsafe_free_space_split: str | None = None,
) -> tuple[m48.M48ObjectQualityPack, m48.M48ObjectTruthSeal]:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()
pack = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(
clips,
unsafe_free_space=unsafe_free_space,
unsafe_free_space_split=unsafe_free_space_split,
),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs",
)
review_a = _review(pack, "reviewer-a")
review_b = _review(pack, "reviewer-b")
review_a_path = tmp_path / "review-a.json"
review_b_path = tmp_path / "review-b.json"
_write_json(review_a_path, review_a)
_write_json(review_b_path, review_b)
adjudication = {
"schema_version": m48.M48_ADJUDICATION_SCHEMA,
"pack_id": pack.result_id,
"state": "completed-adjudicated",
"adjudicator_id": "adjudicator-1",
"review_submission_sha256": sorted(
(m48._canonical_sha256(review_a), m48._canonical_sha256(review_b))
),
"clips": _review_clips(pack.clips, state="adjudicated"),
"acceptance": {
"all_clips_adjudicated": True,
"all_disagreements_resolved": True,
"sealed_at_utc": "2026-08-24T12:00:00Z",
},
}
adjudication_path = tmp_path / "adjudication.json"
_write_json(adjudication_path, adjudication)
truth = m48.build_m48_object_truth_seal(
pack_root=pack.result_root,
reviewer_a_path=review_a_path,
reviewer_b_path=review_b_path,
adjudication_path=adjudication_path,
output_root=tmp_path / "truth",
)
return pack, truth
def test_m48_pack_is_neutral_tracklet_review_evidence(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack, truth = _build_generations(tmp_path, monkeypatch)
reviewer_package = json.loads(
(pack.result_root / "reviewer-package.json").read_text(encoding="utf-8")
)
assert reviewer_package["strata_included"] is False
assert reviewer_package["frozen_predictions_included"] is False
assert all(
"strata" not in clip and "selection_hypotheses" not in clip
for clip in reviewer_package["clips"]
)
assert {
hypothesis
for clip in pack.clips
if clip["split"] == "validation"
for hypothesis in clip["selection_hypotheses"]
} == set(pack.manifest["identity"]["profile"]["required_validation_hypotheses"])
review_template = json.loads(
(pack.result_root / "review-template.json").read_text(encoding="utf-8")
)
assert "clips" in review_template and "frames" not in review_template
assert "tracklets" in review_template["clips"][0]
assert len(truth.truth_rows) == len(pack.frame_references)
assert truth.truth_rows[0]["objects"][0]["visibility"] == "visible"
assert truth.truth_rows[50]["objects"][0]["visibility"] == "partial"
def test_m48_perfect_class_free_result_passes_all_gates_and_registry(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack, truth = _build_generations(tmp_path, monkeypatch)
result = m48.score_m48_object_quality(
pack_root=pack.result_root,
truth_seal_root=truth.result_root,
output_root=tmp_path / "results",
)
assert result.report["acceptance"]["accepted"] is True
assert all(result.report["acceptance"]["gates"].values())
assert result.report["acceptance"]["scope"] == "validation-only"
assert result.report["metrics"] == result.report["metrics_by_split"]["validation"]
assert result.report["method"]["semantic_class_scored"] is False
assert result.report["decision"]["next_gate"] == ("m4.9-recorded-realtime-release-candidate")
repository_root = Path(__file__).resolve().parents[1]
registry = LaboratoryEvidenceRegistry.from_directory(repository_root / "config/laboratories")
definitions = {definition.work_id: definition for definition in registry.definitions}
pack_proof = verify_laboratory_evidence_result(
definitions["m48-object-centric-quality"], pack.result_root
)
result_proof = verify_laboratory_evidence_result(
definitions["m48-object-centric-quality"], result.result_root
)
assert pack_proof["artifact_count"] == 7
assert result_proof["artifact_count"] == 3
def test_m48_review_rejects_semantic_class_and_same_reviewer(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()
pack = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(clips, unsafe_free_space=False),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs",
)
review_a = _review(pack, "reviewer-a")
review_a["clips"][0]["tracklets"][0]["category"] = "car"
review_a_path = tmp_path / "review-a.json"
_write_json(review_a_path, review_a)
with pytest.raises(m48.M48ObjectQualityError, match="fields"):
m48.validate_m48_review_submission(pack_root=pack.result_root, review_path=review_a_path)
review_a = _review(pack, "reviewer-a")
review_b = copy.deepcopy(review_a)
review_a_path = tmp_path / "review-a-clean.json"
review_b_path = tmp_path / "review-b-same.json"
_write_json(review_a_path, review_a)
_write_json(review_b_path, review_b)
adjudication = {
"schema_version": m48.M48_ADJUDICATION_SCHEMA,
"pack_id": pack.result_id,
"state": "completed-adjudicated",
"adjudicator_id": "adjudicator-1",
"review_submission_sha256": [m48._canonical_sha256(review_a)] * 2,
"clips": _review_clips(pack.clips, state="adjudicated"),
"acceptance": {
"all_clips_adjudicated": True,
"all_disagreements_resolved": True,
"sealed_at_utc": "2026-08-24T12:00:00Z",
},
}
adjudication_path = tmp_path / "adjudication.json"
_write_json(adjudication_path, adjudication)
with pytest.raises(m48.M48ObjectQualityError, match="must differ"):
m48.build_m48_object_truth_seal(
pack_root=pack.result_root,
reviewer_a_path=review_a_path,
reviewer_b_path=review_b_path,
adjudication_path=adjudication_path,
output_root=tmp_path / "truth",
)
def test_m48_unsafe_free_space_fails_with_bounded_atlas(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack, truth = _build_generations(tmp_path, monkeypatch, unsafe_free_space=True)
result = m48.score_m48_object_quality(
pack_root=pack.result_root,
truth_seal_root=truth.result_root,
output_root=tmp_path / "results",
)
assert result.report["acceptance"]["accepted"] is False
assert result.report["acceptance"]["gates"]["false_free_space_claims"] is False
assert result.report["acceptance"]["gates"]["critical_corridor_obstacle_recall"] is True
assert result.failure_atlas
assert any("false-free-space-claim" in row["causes"] for row in result.failure_atlas)
assert result.report["decision"]["next_gate"] == (
"bounded-cause-remediation-on-failed-m48-clusters"
)
def test_m48_development_failures_are_reported_but_cannot_fail_release(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack, truth = _build_generations(
tmp_path,
monkeypatch,
unsafe_free_space=True,
unsafe_free_space_split="development",
)
result = m48.score_m48_object_quality(
pack_root=pack.result_root,
truth_seal_root=truth.result_root,
output_root=tmp_path / "results",
)
assert result.report["acceptance"]["accepted"] is True
assert result.report["metrics_by_split"]["development"]["false_free_space_claims"] > 0
assert result.report["metrics"]["false_free_space_claims"] == 0
assert all(result.report["acceptance"]["gates"].values())
assert any(row["split"] == "development" for row in result.failure_atlas)
def test_m48_rejects_incomplete_clip_contract(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()[:19]
with pytest.raises(m48.M48ObjectQualityError, match="20–30"):
m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(clips, unsafe_free_space=False),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs",
)
def test_m48_rejects_cross_split_and_vacuous_grouping(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()
clips[10]["route_block"] = clips[0]["route_block"]
with pytest.raises(m48.M48ObjectQualityError, match="route_block crosses"):
m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(clips, unsafe_free_space=False),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-cross-split",
)
clips = _clips()
for index, clip in enumerate(clips):
clip["component_id"] = f"unique-component-{index:02d}"
with pytest.raises(m48.M48ObjectQualityError, match="non-vacuous"):
m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(clips, unsafe_free_space=False),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-vacuous",
)
def test_m48_profile_config_matches_executable_contract() -> None:
repository_root = Path(__file__).resolve().parents[1]
document = json.loads(
(repository_root / "config/perception/m48-object-quality-v1.json").read_text(
encoding="utf-8"
)
)
profile = m48.DEFAULT_M48_OBJECT_QUALITY_PROFILE
assert document["schema_version"] == m48.M48_PROFILE_SCHEMA
assert document["profile_id"] == profile.profile_id
assert document["clip_contract"]["minimum_clip_count"] == profile.minimum_clip_count
assert document["clip_contract"]["maximum_clip_count"] == profile.maximum_clip_count
assert document["review_contract"]["review_unit"] == "clip-local-object-tracklet"
assert document["review_contract"]["semantic_class_labels_allowed"] is False
assert document["review_contract"]["selection_hypotheses_visible_to_reviewers"] is False
assert document["clip_contract"]["release_gate_split"] == "validation"
assert document["clip_contract"]["required_validation_hypotheses"] == sorted(
m48.DEFAULT_M48_OBJECT_QUALITY_PROFILE.to_dict()["required_validation_hypotheses"]
)
assert document["release_thresholds"]["obstacle_presence_precision"] == (
profile.obstacle_presence_precision
)
assert document["release_thresholds"]["critical_corridor_obstacle_recall"] == (
profile.critical_corridor_obstacle_recall
)
def test_m48_pack_identity_is_stable_across_clip_and_prediction_input_order(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()
predictions = _prediction_rows(clips, unsafe_free_space=False)
first = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=predictions,
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-a",
)
second = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=reversed(clips),
predictions=reversed(predictions),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-b",
)
assert first.result_id == second.result_id
assert first.manifest["identity_sha256"] == second.manifest["identity_sha256"]
changed_provenance = _preparation_provenance()
changed_provenance["camera_index"]["sha256"] = "d" * 64
third = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=predictions,
preparation_provenance=changed_provenance,
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-c",
)
assert third.result_id != first.result_id
assert third.manifest["identity"]["preparation"]["camera_index"]["sha256"] == ("d" * 64)
File diff suppressed because it is too large Load Diff
+355
View File
@@ -0,0 +1,355 @@
from __future__ import annotations
import hashlib
import json
from itertools import pairwise
from pathlib import Path
from types import SimpleNamespace
from k1link.laboratory.m47_reference_graph import M47_REFERENCE_GRAPH_LAB_SCHEMA
from k1link.laboratory.m48_object_quality import read_m48_object_quality_pack
from k1link.laboratory.m48_ravnoves00_pack import (
M48_FRAME_COUNT,
M48_SELECTION_SCHEMA,
_prediction_objects,
prepare_m48_ravnoves00_pack,
)
def _canonical_json(value: object) -> str:
return json.dumps(value, sort_keys=True, separators=(",", ":"))
def _write_json(path: Path, value: object) -> None:
path.write_text(_canonical_json(value) + "\n", encoding="utf-8")
def _write_jsonl(path: Path, rows: list[dict[str, object]]) -> str:
raw = "".join(_canonical_json(row) + "\n" for row in rows).encode()
path.write_bytes(raw)
return hashlib.sha256(raw).hexdigest()
def _recursive_keys(value: object) -> set[str]:
if isinstance(value, dict):
return set(value) | {key for item in value.values() for key in _recursive_keys(item)}
if isinstance(value, list):
return {key for item in value for key in _recursive_keys(item)}
return set()
def test_prediction_projection_is_class_free_and_conservative() -> None:
rows = _prediction_objects(
[
{
"proposal_id": "proposal-0-1",
"bbox_xyxy": [80.0, 60.0, 400.0, 300.0],
"occupied_support": False,
"threat_decision": "unknown",
"semantic_hint": "person",
"objectness": 0.99,
},
{
"proposal_id": "proposal-0-2",
"bbox_xyxy": [400.0, 300.0, 720.0, 540.0],
"occupied_support": True,
"threat_decision": "threat",
"semantic_hint": "car",
"objectness": 0.98,
},
],
geometry_observations=[
{
"proposal_ids": ["proposal-0-1"],
"currentness": "current",
"metric_geometry": None,
},
{
"proposal_ids": ["proposal-0-2"],
"currentness": "current",
"metric_geometry": {"centroid_xyz_m": [1.0, 2.0, 3.0]},
},
],
metric_obstacles=[
{
"centroid_map_xyz_m": [1.0, 2.0, 3.0],
"motion": "moving",
"assessment": {"decision": "threat"},
}
],
)
assert rows == [
{
"prediction_id": "proposal-0-1",
"extent_xyxy": [0.1, 0.1, 0.5, 0.5],
"geometry_association": "unknown",
"freshness": "current",
"motion": "unsupported",
"threat": "unknown",
"unknown_causes": [
"insufficient-geometry-support",
"threat-evidence-insufficient",
],
},
{
"prediction_id": "proposal-0-2",
"extent_xyxy": [0.5, 0.5, 0.9, 0.9],
"geometry_association": "associated",
"freshness": "current",
"motion": "moving",
"threat": "threat",
"unknown_causes": [],
},
]
assert "semantic_hint" not in _canonical_json(rows)
assert "objectness" not in _canonical_json(rows)
def test_real_selection_contract_is_balanced_and_prediction_blind() -> None:
repository_root = Path(__file__).resolve().parents[1]
document = json.loads(
(repository_root / "config/perception/m48-object-quality-selection-v1.json").read_text()
)
clips = document["clips"]
assert document["schema_version"] == M48_SELECTION_SCHEMA
assert len(clips) == 24
assert {clip["split"] for clip in clips} == {"development", "validation"}
assert all("strata" not in clip and "hypotheses" not in clip for clip in clips)
assert document["selection_hypothesis_profile"] == {
"derivation": "exact-frozen-prediction-rows-before-independent-truth",
"small_obstacle_max_normalized_area": 0.001,
"fisheye_edge_margin_normalized": 0.08,
"sparse_scene_max_median_prediction_count": 2.0,
}
for field in ("component_id", "route_block", "time_block"):
group_splits: dict[str, set[str]] = {}
for clip in clips:
group_splits.setdefault(clip[field], set()).add(clip["split"])
assert all(len(splits) == 1 for splits in group_splits.values())
assert len(group_splits) < len(clips)
assert all(left["end_sequence"] < right["start_sequence"] for left, right in pairwise(clips))
forbidden = {"label", "labels", "truth", "review", "adjudication"}
assert forbidden.isdisjoint(document)
def test_prepare_pack_binds_all_source_ledgers_and_freezes_selected_frames(
tmp_path: Path,
monkeypatch,
) -> None:
repository_root = Path(__file__).resolve().parents[1]
graph_root = tmp_path / ("m47-reference-graph-" + "a" * 64)
threat_root = tmp_path / ("m4-threat-replay-" + "b" * 64)
geometry_root = tmp_path / ("m4-geometry-replay-" + "e" * 64)
lab_root = tmp_path / ("m47-reference-graph-lab-" + "c" * 64)
graph_root.mkdir()
threat_root.mkdir()
geometry_root.mkdir()
lab_root.mkdir()
_write_json(lab_root / "manifest.json", {"fixture": True})
graph_rows: list[dict[str, object]] = []
threat_rows: list[dict[str, object]] = []
geometry_rows: list[dict[str, object]] = []
camera_rows: list[dict[str, object]] = []
for frame_index in range(M48_FRAME_COUNT):
source_time_ns = 35_421_857_292 + frame_index * 100_000_000
graph_rows.append(
{
"sequence": frame_index,
"obstacle_map": {
"schema_version": "missioncore.local-obstacle-map/v1",
"frame_id": f"frame-{frame_index:06d}",
"free_space_claimed": False,
},
"threats": [],
}
)
threat_rows.append(
{
"schema_version": "missioncore.perception-threat-replay-frame/v2",
"sequence": frame_index,
"frame_id": f"frame-{frame_index:06d}",
"source_time_ns": source_time_ns,
"source_available": True,
"camera_proposals": [
{
"proposal_id": f"proposal-{frame_index}-0",
"bbox_xyxy": [0.0, 0.0, 20.0, 20.0],
"occupied_support": True,
"threat_decision": "threat" if frame_index % 2 == 0 else "not-threat",
},
{
"proposal_id": f"proposal-{frame_index}-1",
"bbox_xyxy": [80.0, 60.0, 400.0, 300.0],
"occupied_support": False,
"threat_decision": "unknown",
},
],
"metric_obstacles": [
{
"centroid_map_xyz_m": [1.0, 2.0, 3.0],
"motion": "moving" if frame_index % 2 == 0 else "stationary",
"assessment": {
"decision": "threat" if frame_index % 2 == 0 else "not-threat"
},
}
],
}
)
geometry_rows.append(
{
"schema_version": "missioncore.perception-geometry-replay-frame/v1",
"sequence": frame_index,
"frame_id": f"frame-{frame_index:06d}",
"source_available": True,
"observations": [
{
"proposal_ids": [f"proposal-{frame_index}-0"],
"currentness": "current",
"metric_geometry": {"centroid_xyz_m": [1.0, 2.0, 3.0]},
},
{
"proposal_ids": [f"proposal-{frame_index}-1"],
"currentness": "current",
"metric_geometry": None,
},
],
}
)
camera_rows.append(
{
"schema_version": "missioncore.camera-recording-index/v1",
"sequence": frame_index + 1,
"kind": "media",
"session_monotonic_ns": frame_index + 1,
"sha256": hashlib.sha256(f"camera-{frame_index}".encode()).hexdigest(),
}
)
graph_sha256 = _write_jsonl(graph_root / "frames.jsonl", graph_rows)
threat_sha256 = _write_jsonl(threat_root / "frames.jsonl", threat_rows)
geometry_sha256 = _write_jsonl(geometry_root / "frames.jsonl", geometry_rows)
camera_index = tmp_path / "index.jsonl"
_write_jsonl(camera_index, camera_rows)
_write_json(
graph_root / "manifest.json",
{
"schema_version": "missioncore.reference-perception-graph-manifest/v1",
"result_id": graph_root.name,
"accepted": True,
"graph_id": "reference-perception-graph/v2",
"run_mode": "lossless-replay",
"files": {
"frames.jsonl": {
"bytes": (graph_root / "frames.jsonl").stat().st_size,
"sha256": graph_sha256,
}
},
},
)
_write_json(
threat_root / "manifest.json",
{
"schema_version": "missioncore.perception-threat-replay-result/v2",
"result_id": threat_root.name,
"accepted": True,
"identity": {
"source_session_id": "20260720T065719Z_viewer_live",
"frames_sha256": threat_sha256,
"geometry_result_id": geometry_root.name,
"geometry_frames_sha256": geometry_sha256,
},
},
)
_write_json(
geometry_root / "manifest.json",
{
"schema_version": "missioncore.perception-geometry-replay-result/v1",
"identity": {
"accepted": True,
"source_pack_id": (
"e10-lidar-pack-"
"576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
),
"frames_sha256": geometry_sha256,
},
},
)
authority = {
"mode": "replay-simulated",
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}
lab = SimpleNamespace(
result_id=lab_root.name,
result_root=lab_root,
manifest={
"schema_version": M47_REFERENCE_GRAPH_LAB_SCHEMA,
"accepted": True,
"ground_truth": False,
},
report={
"source": {
"graph_result_id": graph_root.name,
"visual_result_id": threat_root.name,
"threat_frames_sha256": threat_sha256,
"source_id": "RAVNOVES00",
"source_session_id": "20260720T065719Z_viewer_live",
},
"method": {
"graph_id": "reference-perception-graph/v2",
"run_mode": "lossless-replay",
"canonical_payload_sha256": "d" * 64,
},
"decision": {
"state": "accepted-reference-graph-replay",
"next_gate": "independent-object-centric-detection-quality",
},
"acceptance": {"accepted": True},
"authority": authority,
},
)
monkeypatch.setattr(
"k1link.laboratory.m48_ravnoves00_pack.read_m47_reference_graph_lab",
lambda _: lab,
)
monkeypatch.setattr(
"k1link.laboratory.m48_object_quality.read_m47_reference_graph_lab",
lambda _: lab,
)
result = prepare_m48_ravnoves00_pack(
m47_lab_root=lab_root,
graph_result_root=graph_root,
threat_result_root=threat_root,
geometry_result_root=geometry_root,
camera_index_path=camera_index,
selection_path=(repository_root / "config/perception/m48-object-quality-selection-v1.json"),
frozen_at_utc="2026-08-24T00:00:00Z",
output_root=tmp_path / "runtime/m48/object-quality-packs",
)
assert read_m48_object_quality_pack(result.result_root) == result
assert result.report["metrics"]["clip_count"] == 24
assert result.report["metrics"]["frame_count"] == 24 * 61
assert len(result.predictions) == 24 * 61
assert result.manifest["identity"]["preparation"]["adapter"]["sha256"] == (
hashlib.sha256(
(repository_root / "src/k1link/laboratory/m48_ravnoves00_pack.py").read_bytes()
).hexdigest()
)
assert result.manifest["identity"]["preparation"]["selection"]["sha256"] == (
hashlib.sha256(
(
repository_root / "config/perception/m48-object-quality-selection-v1.json"
).read_bytes()
).hexdigest()
)
reviewer_package = json.loads((result.result_root / "reviewer-package.json").read_text())
reviewer_keys = _recursive_keys(reviewer_package)
assert "strata" not in reviewer_keys
assert "prediction_id" not in reviewer_keys
assert "semantic_hint" not in reviewer_keys
+350
View File
@@ -0,0 +1,350 @@
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from pathlib import Path
from types import SimpleNamespace
from typing import cast
import numpy as np
import pytest
from k1link.laboratory import m48_raw_evidence as raw_module
from k1link.laboratory.m48_object_quality import M48ObjectQualityPack
from k1link.laboratory.m48_raw_evidence import (
M48_EXPECTED_FRAME_COUNT,
M48_EXPECTED_SESSION_ID,
M48_EXPECTED_SOURCE_ID,
M48_RAW_SPATIAL_FRAME_SCHEMA,
M48RawEvidenceError,
M48RawEvidenceReader,
)
from k1link.perception.geometry import RecordedFrameTemporalBinding
from k1link.perception.threat import ReplayBodyFrame, load_replay_threat_profile
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m4-replay-threat-v3.json"
def _canonical_sha256(value: object) -> str:
return hashlib.sha256(
json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode()
).hexdigest()
def _sealed_e10_pack(tmp_path: Path) -> tuple[Path, str, str]:
payload = b"sealed-e10-lidar-pack"
artifact_sha256 = hashlib.sha256(payload).hexdigest()
identity = {
"available_lidar_frames": 3928,
"calibration_sha256": "1" * 64,
"camera_slot": "camera_1",
"e6_profile_sha256": "2" * 64,
"e6_result_id": "e6-fixture",
"frame_count": M48_EXPECTED_FRAME_COUNT,
"input_sha256": "3" * 64,
"job_id": "recorded-camera-fixture",
"point_count": 5,
"producer_sha256": "4" * 64,
"projection": {
"height": 600,
"model": "kb4",
"source_coordinates": "k1-map",
"target_camera": "sensor.camera.right",
"width": 800,
},
"schema_version": "missioncore.e10-lidar-replay-pack/v1",
"semantic_timeline_result_id": "result-fixture",
"session_id": M48_EXPECTED_SESSION_ID,
"source_end_frame_index": M48_EXPECTED_FRAME_COUNT - 1,
"source_id": "sensor.camera.right",
"source_start_frame_index": 0,
"temporal_binding": "accepted-e6-nearest-host-arrival-best-effort",
"temporal_policy": {
"binding": "nearest-host-arrival-best-effort",
"clock_source": "recorded-host-monotonic-arrival",
"maximum_lidar_camera_delta_ms": 100.0,
"maximum_pose_point_delta_ms": 100.0,
},
"timeline_end_seconds": 484.0,
"timeline_start_seconds": 35.0,
}
identity_sha256 = _canonical_sha256(identity)
pack_id = f"e10-lidar-pack-{identity_sha256}"
root = tmp_path / pack_id
root.mkdir()
(root / "lidar-pack.npz").write_bytes(payload)
manifest = {
"artifact": {
"byte_length": len(payload),
"media_type": "application/x-npz",
"path": "lidar-pack.npz",
"sha256": artifact_sha256,
},
"classification": "private-recorded-sensor-replay-input",
"created_at_utc": "2026-07-22T06:05:22.515Z",
"ground_truth": False,
"identity": identity,
"identity_sha256": identity_sha256,
"pack_id": pack_id,
"schema_version": "missioncore.e10-lidar-replay-pack/v1",
}
(root / "manifest.json").write_text(
json.dumps(manifest, sort_keys=True, separators=(",", ":")),
encoding="utf-8",
)
return root, pack_id, artifact_sha256
def test_e10_pack_validation_binds_identity_path_length_and_sha256(tmp_path: Path) -> None:
root, pack_id, artifact_sha256 = _sealed_e10_pack(tmp_path)
artifact = raw_module._validate_e10_pack(
root,
expected_pack_id=pack_id,
expected_artifact_sha256=artifact_sha256,
)
assert artifact == (root / "lidar-pack.npz").resolve()
(root / "lidar-pack.npz").write_bytes(b"tampered")
with pytest.raises(M48RawEvidenceError, match="artifact content changed"):
raw_module._validate_e10_pack(
root,
expected_pack_id=pack_id,
expected_artifact_sha256=artifact_sha256,
)
@pytest.mark.parametrize(
("mutation", "message"),
[
(lambda manifest: manifest["identity"].update(session_id="other"), "identity changed"),
(
lambda manifest: manifest["artifact"].update(path="../lidar-pack.npz"),
"identity changed",
),
(lambda manifest: manifest.update(pack_id="e10-lidar-pack-wrong"), "identity changed"),
],
)
def test_e10_pack_validation_rejects_manifest_escape(
tmp_path: Path,
mutation: object,
message: str,
) -> None:
root, pack_id, artifact_sha256 = _sealed_e10_pack(tmp_path)
manifest_path = root / "manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
assert callable(mutation)
mutation(manifest)
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
with pytest.raises(M48RawEvidenceError, match=message):
raw_module._validate_e10_pack(
root,
expected_pack_id=pack_id,
expected_artifact_sha256=artifact_sha256,
)
@dataclass
class _Store:
source_time_ns: int = 2_000_000_000
source_available: bool = True
def temporal_binding_for_index(self, frame_index: int) -> RecordedFrameTemporalBinding:
return RecordedFrameTemporalBinding(
frame_index=frame_index,
source_time_ns=self.source_time_ns,
source_available=self.source_available,
lidar_camera_delta_ms=1.0 if self.source_available else None,
pose_point_delta_ms=1.0 if self.source_available else None,
)
def current_points_for_frame(self, frame_index: int) -> np.ndarray:
del frame_index
return np.asarray(
[
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[4.0, 0.0, 0.0],
[5.0, 0.0, 0.0],
],
dtype=np.float64,
)
@dataclass
class _BodyFrames:
available: bool = True
def body_frame_for_frame(self, frame_id: str) -> ReplayBodyFrame | None:
if not self.available:
return None
return ReplayBodyFrame(
frame_id=frame_id,
origin_map_xyz_m=(1.0, 0.0, 0.0),
basis_map_from_body=((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
sensor_height_m=1.25,
surface_slope_deg=0.0,
forward_source="fixture",
camera_forward_alignment_deg=0.0,
)
class _PredictionTrapPack:
result_id = "m48-object-quality-pack-" + "a" * 64
result_root = REPOSITORY_ROOT
manifest = {
"identity": {
"source": {
"source_id": M48_EXPECTED_SOURCE_ID,
"source_session_id": M48_EXPECTED_SESSION_ID,
}
}
}
report: dict[str, object] = {}
clips: tuple[dict[str, object], ...] = ()
frame_references = (
{
"clip_id": "clip-01",
"sequence": 2,
"source_time_ns": 2_000_000_000,
},
)
@property
def predictions(self) -> object:
raise AssertionError("neutral raw reader opened frozen predictions")
def _reader(
tmp_path: Path,
*,
store: _Store | None = None,
body_frames: _BodyFrames | None = None,
) -> M48RawEvidenceReader:
profile = load_replay_threat_profile(PROFILE_PATH)
timeline = SimpleNamespace(
store=store or _Store(),
body_frames=body_frames or _BodyFrames(),
profile=profile,
)
threat = SimpleNamespace(
result_id="m4-threat-replay-fixture",
result_root=tmp_path,
manifest={"identity": {"frames_sha256": "f" * 64}},
)
return M48RawEvidenceReader(
repository_root=tmp_path,
threat_result=threat,
timeline=timeline,
point_limit=2,
)
def test_raw_reader_is_one_based_bounded_body_frame_and_prediction_free(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(raw_module, "_validate_m47_pack_binding", lambda **_: None)
reader = _reader(tmp_path)
pack = cast(M48ObjectQualityPack, _PredictionTrapPack())
frame = reader(pack, 2)
assert set(frame) == {
"schema_version",
"pack_id",
"clip_id",
"sequence",
"source_time_ns",
"source_available",
"body_frame_available",
"point_cloud_body_xyz_m",
"rig",
"corridor",
"occupied_voxel_size_m",
"candidate_identity_included",
"graph_boxes_ids_scores_included",
"frozen_predictions_included",
"strata_included",
"authority",
}
assert frame["schema_version"] == M48_RAW_SPATIAL_FRAME_SCHEMA
assert frame["clip_id"] == "clip-01"
assert frame["sequence"] == 2
assert frame["source_available"] is True
assert frame["body_frame_available"] is True
assert frame["point_cloud_body_xyz_m"] == [[0.0, 0.0, 0.0], [3.0, 0.0, 0.0]]
assert len(cast(list[object], frame["point_cloud_body_xyz_m"])) <= 2
assert frame["candidate_identity_included"] is False
assert frame["graph_boxes_ids_scores_included"] is False
assert frame["frozen_predictions_included"] is False
assert frame["strata_included"] is False
assert "metric_obstacles" not in frame
assert "camera_proposals" not in frame
assert "decision_counts" not in frame
assert "body_frame" not in frame
def test_raw_reader_fails_closed_on_clip_or_source_time_escape(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(raw_module, "_validate_m47_pack_binding", lambda **_: None)
pack = cast(M48ObjectQualityPack, _PredictionTrapPack())
reader = _reader(tmp_path)
with pytest.raises(M48RawEvidenceError, match="outside the selected neutral clips"):
reader(pack, 1)
mismatched = _reader(tmp_path, store=_Store(source_time_ns=2_000_000_001))
with pytest.raises(M48RawEvidenceError, match="source time escaped"):
mismatched(pack, 2)
def test_raw_reader_emits_empty_cloud_when_body_frame_is_unavailable(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(raw_module, "_validate_m47_pack_binding", lambda **_: None)
reader = _reader(
tmp_path,
store=_Store(source_available=False),
body_frames=_BodyFrames(available=False),
)
pack = cast(M48ObjectQualityPack, _PredictionTrapPack())
frame = reader.frame(pack=pack, sequence=2)
assert frame["source_available"] is False
assert frame["body_frame_available"] is False
assert frame["point_cloud_body_xyz_m"] == []
assert isinstance(frame["rig"], dict)
assert isinstance(frame["corridor"], dict)
@pytest.mark.parametrize("point_limit", [0, 4097, True])
def test_raw_reader_rejects_unbounded_point_limits(
tmp_path: Path,
point_limit: int,
) -> None:
profile = load_replay_threat_profile(PROFILE_PATH)
timeline = SimpleNamespace(store=_Store(), body_frames=_BodyFrames(), profile=profile)
threat = SimpleNamespace(result_id="fixture", result_root=tmp_path, manifest={})
with pytest.raises(M48RawEvidenceError, match="point limit"):
M48RawEvidenceReader(
repository_root=tmp_path,
threat_result=threat,
timeline=timeline,
point_limit=point_limit,
)
+236
View File
@@ -0,0 +1,236 @@
from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
import pytest
import k1link.laboratory.m48_small_static_regression as regression
from k1link.laboratory.evidence_registry import LaboratoryEvidenceRegistry
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
AUTHORITY = {
"mode": "replay-simulated",
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}
def _canonical(value: object) -> bytes:
return json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
).encode()
def _write_json(path: Path, value: object) -> None:
path.write_bytes(_canonical(value) + b"\n")
def _correction(pack_id: str) -> dict[str, object]:
def tracklet(object_id: str, extent: list[float], *, passage: bool) -> dict[str, object]:
return {
"object_id": object_id,
"first_sequence": 10,
"last_sequence": 10,
"keyframes": [{
"sequence": 10,
"extent_xyxy": extent,
"visibility": "visible",
}],
"state_segments": [{
"start_sequence": 10,
"end_sequence": 10,
"geometry_association": "unknown",
"freshness": "current",
"motion": "static",
"threat": "not-threat",
"critical_corridor_obstacle": passage,
}],
"notes": None,
}
return {
"schema_version": "missioncore.m48-assisted-object-correction-session/v1",
"pack_id": pack_id,
"session_id": "m48-correction-session-" + "b" * 64,
"title": "fixture",
"revision": 7,
"state": "saved",
"created_at_utc": "2026-08-24T10:00:00Z",
"updated_at_utc": "2026-08-24T11:00:00Z",
"clips": [{
"clip_id": "m48-clip-01",
"start_sequence": 1,
"end_sequence": 20,
"review_state": "reviewed",
"no_object": False,
"tracklets": [
tracklet("object-01", [0.1, 0.1, 0.2, 0.2], passage=True),
tracklet("object-02", [0.7, 0.7, 0.8, 0.8], passage=False),
{
**tracklet("object-03", [0.3, 0.3, 0.4, 0.4], passage=True),
"object_id": "proposal-10-0",
},
],
"notes": None,
}],
"progress": {"reviewed_clip_count": 1, "clip_count": 1, "complete": True},
"seed_summary": {
"worker_id": "006",
"clip_count": 1,
"frame_count": 1,
"object_count": 1,
"prediction_rows_sha256": "c" * 64,
},
"evidence_summary": None,
"assistance": {
"mode": "frozen-candidate-seeded",
"candidate_predictions_seen": True,
"model_scores_seen": False,
"semantic_class_task_seen": False,
"independent_truth_eligible": False,
},
"authority": AUTHORITY,
"last_save_idempotency_key": "save-7",
"reviewer_id": None,
"submitted_at_utc": None,
"submission_sha256": None,
"frozen_document_name": None,
}
def test_builds_separate_assisted_baseline_without_truth_claim(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack_id = "m48-object-quality-pack-" + "a" * 64
pack_root = tmp_path / pack_id
pack_root.mkdir()
pack = SimpleNamespace(
result_id=pack_id,
result_root=pack_root,
manifest={
"identity_sha256": "a" * 64,
"identity": {
"source": {
"source_id": "RAVNOVES00",
"source_session_id": "source-session",
},
"freeze": {"prediction_rows_sha256": "c" * 64},
},
},
predictions=({
"schema_version": "missioncore.m48-frozen-prediction-row/v1",
"clip_id": "m48-clip-01",
"sequence": 10,
"source_time_ns": 100,
"terminal_outcome": "delivered",
"terminal_reason": None,
"free_space_claimed": False,
"objects": [{
"prediction_id": "proposal-10-0",
"extent_xyxy": [0.1, 0.1, 0.2, 0.2],
"geometry_association": "unknown",
"freshness": "current",
"motion": "static",
"threat": "not-threat",
}],
},),
)
monkeypatch.setattr(regression, "read_m48_object_quality_pack", lambda _: pack)
correction_path = tmp_path / "correction.json"
_write_json(correction_path, _correction(pack_id))
profile_path = REPOSITORY_ROOT / "config/perception/m48-small-static-passage-regression-v1.json"
result = regression.build_m48_small_static_passage_regression(
pack_root=pack_root,
correction_session_path=correction_path,
profile_path=profile_path,
output_root=tmp_path / "results",
run_created_at_utc="2026-08-24T12:00:00Z",
)
assert result.report["metrics"]["assisted_anchor_count"] == 2
assert result.report["metrics"]["worker_recalled_anchor_count"] == 1
assert result.report["metrics"]["worker_missed_anchor_count"] == 1
assert result.report["metrics"]["assisted_anchor_recall"] == 0.5
assert result.manifest["accepted"] is False
assert result.manifest["ground_truth"] is False
assert result.manifest["identity"]["human_lab_id"] == "M4.8"
assert result.manifest["identity"]["experiment_id"] == (
"m48-small-static-passage-regression/v1"
)
assert result.report["method"]["execution_class"] == "deterministic"
assert all(
row["authority"] == "operator-assisted-development-anchor-not-truth"
for row in result.anchors
)
registry = LaboratoryEvidenceRegistry.from_directory(
REPOSITORY_ROOT / "config/laboratories"
)
definition = next(
row
for row in registry.definitions
if row.work_id == "m48-small-static-passage-regression"
)
proof = verify_laboratory_evidence_result(definition, result.result_root)
assert proof["result_id"] == result.result_id
assert proof["artifact_count"] == 3
def test_reader_rejects_changed_comparison_artifact(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack_id = "m48-object-quality-pack-" + "a" * 64
pack_root = tmp_path / pack_id
pack_root.mkdir()
pack = SimpleNamespace(
result_id=pack_id,
result_root=pack_root,
manifest={
"identity_sha256": "a" * 64,
"identity": {
"source": {"source_id": "RAVNOVES00", "source_session_id": "source"},
"freeze": {"prediction_rows_sha256": "c" * 64},
},
},
predictions=({
"clip_id": "m48-clip-01",
"sequence": 10,
"source_time_ns": 100,
"terminal_outcome": "delivered",
"objects": [],
},),
)
monkeypatch.setattr(regression, "read_m48_object_quality_pack", lambda _: pack)
correction_path = tmp_path / "correction.json"
document = _correction(pack_id)
document["clips"][0]["tracklets"] = document["clips"][0]["tracklets"][:1]
_write_json(correction_path, document)
result = regression.build_m48_small_static_passage_regression(
pack_root=pack_root,
correction_session_path=correction_path,
profile_path=(
REPOSITORY_ROOT
/ "config/perception/m48-small-static-passage-regression-v1.json"
),
output_root=tmp_path / "results",
run_created_at_utc="2026-08-24T12:00:00Z",
)
comparison_path = result.result_root / "comparisons.jsonl"
comparison_path.write_bytes(comparison_path.read_bytes() + b"{}\n")
with pytest.raises(
regression.M48SmallStaticRegressionError,
match="artifact proof",
):
regression.read_m48_small_static_passage_regression(result.result_root)