feat(lab): verify and expose real evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 15:36:48 +03:00
parent 453d760be4
commit dc55ff16c9
9 changed files with 2007 additions and 47 deletions
@@ -44,6 +44,16 @@ import type {
MissionRuntimeState,
ObservationSourceDescriptor,
} from "../core/runtime/contracts";
import {
fetchE29EvidenceCatalog,
fetchE29EvidenceFrame,
type E29EvidenceFrame,
type E29EvidenceResult,
} from "../core/laboratory/e29Evidence";
import {
fetchLidarLocalSurfaces,
type LidarLocalSurfaceModel,
} from "../core/lidar/localSurface";
import {
RerunViewport,
isRecordedPlaybackPresentationReady,
@@ -1271,39 +1281,200 @@ function LaboratoryTask({
);
}
function E29LaboratoryResult({ rigLabel }: { rigLabel: string }) {
function formatSeconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`;
}
function E29LaboratoryResult({
props,
rigLabel,
result,
sourceSession,
loading,
error,
}: {
props: WorkspaceRendererProps;
rigLabel: string;
result: E29EvidenceResult;
sourceSession: ObservationSessionSummary;
loading: boolean;
error: string | null;
}) {
const [selectedFrameIndex, setSelectedFrameIndex] = useState(
result.reviewFrames[0]?.frameIndex ?? 0,
);
const [frame, setFrame] = useState<E29EvidenceFrame | null>(null);
const [frameLoading, setFrameLoading] = useState(false);
const [frameError, setFrameError] = useState<string | null>(null);
const replayReady = props.recordedReplay?.sessionId === sourceSession.id;
const semantic = result.metrics.semanticObservations;
const geometryStatus = semantic.geometryStatus;
const conflicts = frame?.semanticObservations.filter(
(observation) => observation.geometryStatus === "conflict",
) ?? [];
useEffect(() => {
const controller = new AbortController();
setFrameLoading(true);
setFrameError(null);
void fetchE29EvidenceFrame(result.resultId, selectedFrameIndex, {
signal: controller.signal,
}).then((next) => {
setFrame(next);
}).catch((caught: unknown) => {
if (controller.signal.aborted) return;
setFrame(null);
setFrameError(
caught instanceof Error ? caught.message : "Кадр E29 недоступен.",
);
}).finally(() => {
if (!controller.signal.aborted) setFrameLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedFrameIndex]);
return (
<>
<LaboratoryTask
title="LAB E29 · camera-first semantics + независимая геометрия"
description="Проверяется рабочая продуктовая граница: камера сохраняет семантический класс и идентичность объекта, LiDAR подтверждает метрическую дальность и занятую геометрию по локальной поверхности L2.6. Отсутствие точек не объявляется свободным пространством."
status="Диагностический replay"
status="Проверенные артефакты"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · камера + LiDAR · worker D` },
{ label: "Источник", value: "RAVNOVES00 · 4 489 кадров" },
{ label: "Объектов", value: "20 513 наблюдений" },
{ label: "Полномочия", value: "Только shadow · команды запрещены" },
{
label: "Источник",
value: `${sourceSession.label} · ${formatNumber(result.identity.frameCount, 0)} кадров`,
},
{
label: "Наблюдений",
value: formatNumber(semantic.total, 0),
},
{
label: "Артефакты",
value: "Control plane · read-only · hash verified",
},
]}
/>
<section className="lab-result-surface">
<header>
<span className="section-eyebrow">ИСХОДНЫЕ ДАННЫЕ</span>
<strong>LiDAR, траектория и камера RAVNOVES00</strong>
</header>
{replayReady ? (
<SpatialWorkspace {...props} />
) : (
<div className="laboratory-result-pending" role="status">
{loading ? <span className="busy-indicator" aria-hidden="true" /> : <Icon name="database" size={20} />}
<strong>{loading ? "Проверяем и открываем запись" : "Исходная запись не открыта"}</strong>
<p>
{error ?? (loading
? "Viewer появится после серверной проверки неизменяемого RRD."
: "Выберите LAB E29 повторно, чтобы открыть связанный источник.")}
</p>
</div>
)}
</section>
<section className="laboratory-result-summary">
<header>
<div>
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
<h2>Архитектурная схема подтверждена, safety-gate не пройден</h2>
<h2>Camera-first контракт рассчитан, production gate не пройден</h2>
</div>
<StatusBadge tone="success">Replay завершён</StatusBadge>
<StatusBadge tone={result.decision.productionPromotion ? "success" : "warning"}>
{result.decision.productionPromotion ? "Допущено" : "Только диагностика"}
</StatusBadge>
</header>
<div className="laboratory-result-metrics">
<div><span>Поддержка геометрией</span><strong>6 341</strong><small>32,31% current observations</small></div>
<div><span>Camera-only</span><strong>13 246</strong><small>Семантика сохранена</small></div>
<div><span>Postprocess p95</span><strong>2,517 мс</strong><small>8,467 с полный build</small></div>
<div><span>Geometry-only</span><strong>21 321</strong><small>Занято / класс неизвестен</small></div>
<div>
<span>Поддержка геометрией</span>
<strong>{formatNumber(geometryStatus.agree, 0)}</strong>
<small>{(semantic.agreementFractionOfCurrent * 100).toLocaleString("ru-RU", { maximumFractionDigits: 2 })}% current</small>
</div>
<div>
<span>Только камера</span>
<strong>{formatNumber(geometryStatus.cameraOnly, 0)}</strong>
<small>Семантика без LiDAR-подтверждения</small>
</div>
<div>
<span>Postprocess p95</span>
<strong>{result.metrics.runtime.frameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс</strong>
<small>{formatSeconds(result.metrics.runtime.buildElapsedMs / 1000)} полный build</small>
</div>
<div>
<span>Только геометрия</span>
<strong>{formatNumber(result.metrics.geometryOnlyOccupied.clusterCount, 0)}</strong>
<small>{formatNumber(result.metrics.geometryOnlyOccupied.pointCount, 0)} точек</small>
</div>
</div>
<p>
Следующий gate покадровый разбор конфликтов, camera-only пробелов и
geometry-only компонентов, затем source-paced shadow с deadline и staleness.
</p>
<p>{result.decision.nextGate}</p>
</section>
<section className="laboratory-frame-review">
<header>
<div>
<span className="section-eyebrow">КАДРЫ С КОНФЛИКТОМ</span>
<h2>Покадровое доказательство из camera-geometry-frames.jsonl</h2>
</div>
<StatusBadge tone="warning">
{formatNumber(geometryStatus.conflict, 0)} конфликтов
</StatusBadge>
</header>
<div className="laboratory-frame-review__picker" role="list">
{result.reviewFrames.slice(0, 16).map((review) => (
<button
key={review.frameIndex}
type="button"
className={review.frameIndex === selectedFrameIndex ? "is-active" : undefined}
onClick={() => setSelectedFrameIndex(review.frameIndex)}
>
<span>Кадр {formatNumber(review.sourceFrameIndex, 0)}</span>
<small>{formatSeconds(review.sessionSeconds)} · {review.conflictCount} конфликт</small>
</button>
))}
</div>
{frameLoading ? (
<div className="laboratory-frame-review__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Читаем подтверждённый кадр</span>
</div>
) : frameError || !frame ? (
<div className="laboratory-frame-review__state" role="status">
<Icon name="database" size={18} />
<span>{frameError ?? "Кадр недоступен."}</span>
</div>
) : (
<div className="laboratory-frame-review__detail">
<div>
<span>Кадр источника</span>
<strong>{formatNumber(frame.sourceFrameIndex, 0)}</strong>
<small>{formatSeconds(frame.sessionSeconds)}</small>
</div>
<div>
<span>Семантические наблюдения</span>
<strong>{formatNumber(frame.semanticObservations.length, 0)}</strong>
<small>{formatNumber(conflicts.length, 0)} требуют разбора</small>
</div>
<div>
<span>Geometry-only компоненты</span>
<strong>{formatNumber(frame.geometryOnlyOccupied.length, 0)}</strong>
<small>Класс не назначается</small>
</div>
<div className="laboratory-frame-review__conflicts">
<span>Фактические конфликты</span>
{conflicts.length ? conflicts.map((observation) => (
<p key={observation.trackId}>
<strong>{observation.label} · track {observation.trackId}</strong>
<small>
{observation.geometryReason} · classified {observation.support.classifiedPoints}
{" · "}surface {observation.support.surfacePoints}
</small>
</p>
)) : <small>В этом кадре конфликт не найден.</small>}
</div>
</div>
)}
</section>
</>
);
@@ -1421,6 +1592,10 @@ function PublishedLaboratoryResult({
function LabArchiveWorkspace(props: WorkspaceRendererProps) {
const [profileId, setProfileId] = useState<LaboratoryProfileId>("sensor-fusion");
const [workId, setWorkId] = useState<LaboratoryWorkId>("e28-local-surface");
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
const [evidenceLoading, setEvidenceLoading] = useState(true);
const [evidenceError, setEvidenceError] = useState<string | null>(null);
const sessions = useObservationSessions({
limit: 100,
replayEnabled: props.sessionArchive.blockedReason === null,
@@ -1429,9 +1604,47 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
onReplaySettled: props.sessionArchive.onReplaySettled,
});
const publishedWorks = useMemo(
() => sessions.items.filter((session) => session.lab !== null),
() => sessions.items.filter((session) => (
session.lab !== null
&& session.status === "ready"
&& session.replayable
&& session.modalities.includes("point-cloud")
)),
[sessions.items],
);
const sourceSessions = useMemo(
() => new Map(sessions.items.map((session) => [session.id, session])),
[sessions.items],
);
useEffect(() => {
const controller = new AbortController();
setEvidenceLoading(true);
setEvidenceError(null);
void Promise.allSettled([
fetchLidarLocalSurfaces({ signal: controller.signal }),
fetchE29EvidenceCatalog({ signal: controller.signal }),
]).then(([e28, e29]) => {
if (controller.signal.aborted) return;
const nextE28 = e28.status === "fulfilled" ? e28.value.items[0] ?? null : null;
const nextE29 = e29.status === "fulfilled" ? e29.value.items[0] ?? null : null;
setE28Model(nextE28);
setE29Result(nextE29);
const failures = [
e28.status === "rejected" ? "E28" : null,
e29.status === "rejected" ? "E29" : null,
].filter(Boolean);
setEvidenceError(
failures.length
? `${failures.join(" и ")} не прошли серверную проверку и скрыты.`
: null,
);
}).finally(() => {
if (!controller.signal.aborted) setEvidenceLoading(false);
});
return () => controller.abort();
}, []);
const rigLabel = useMemo(() => {
if (props.deviceLabel) return props.deviceLabel;
const coordinateFrame = publishedWorks
@@ -1442,22 +1655,44 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
: "";
return sensorToken ? sensorToken.toLocaleUpperCase("ru-RU") : "Сенсорный риг";
}, [props.deviceLabel, publishedWorks]);
const profiles: readonly LaboratoryOption<LaboratoryProfileId>[] = [
{
id: "sensor-fusion",
label: `${rigLabel} · камера + LiDAR · worker D`,
},
{
id: "published-perception",
label: `${rigLabel} · опубликованный perception pipeline`,
},
];
const sensorWorks = useMemo(() => {
const items: LaboratoryOption<LaboratoryWorkId>[] = [];
if (e28Model) {
items.push({
id: "e28-local-surface",
label: "LAB E28 · локальная поверхность L2.6",
});
}
if (
e29Result
&& sourceSessions.has(e29Result.linkedEvidence.sourceSessionId)
) {
items.push({
id: "e29-camera-geometry",
label: "LAB E29 · camera-first + geometry",
});
}
return items;
}, [e28Model, e29Result, sourceSessions]);
const profiles = useMemo(() => {
const items: LaboratoryOption<LaboratoryProfileId>[] = [];
if (sensorWorks.length) {
items.push({
id: "sensor-fusion",
label: `${rigLabel} · камера + LiDAR · control plane`,
});
}
if (publishedWorks.length) {
items.push({
id: "published-perception",
label: `${rigLabel} · опубликованный perception pipeline`,
});
}
return items;
}, [publishedWorks.length, rigLabel, sensorWorks.length]);
const workOptions: readonly LaboratoryOption<LaboratoryWorkId>[] =
profileId === "sensor-fusion"
? [
{ id: "e28-local-surface", label: "LAB E28 · локальная поверхность L2.6" },
{ id: "e29-camera-geometry", label: "LAB E29 · camera-first + geometry" },
]
? sensorWorks
: publishedWorks.map((session) => ({
id: `session:${session.id}` as const,
label: `${session.lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`,
@@ -1468,18 +1703,49 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
const selectedSession = selectedSessionId
? publishedWorks.find((session) => session.id === selectedSessionId) ?? null
: null;
const e29SourceSession = e29Result
? sourceSessions.get(e29Result.linkedEvidence.sourceSessionId) ?? null
: null;
useEffect(() => {
if (profileId !== "published-perception" || workId.startsWith("session:")) return;
const first = publishedWorks[0];
if (!first) return;
setWorkId(`session:${first.id}`);
}, [profileId, publishedWorks, workId]);
if (
evidenceLoading
|| sessions.state === "idle"
|| sessions.state === "loading"
) return;
if (!profiles.some((profile) => profile.id === profileId)) {
const firstProfile = profiles[0];
if (!firstProfile) return;
setProfileId(firstProfile.id);
if (firstProfile.id === "sensor-fusion") {
const firstWork = sensorWorks[0];
if (firstWork) setWorkId(firstWork.id);
} else {
const first = publishedWorks[0];
if (first) setWorkId(`session:${first.id}`);
}
return;
}
if (!workOptions.some((work) => work.id === workId)) {
const firstWork = workOptions[0];
if (firstWork) setWorkId(firstWork.id);
}
}, [
evidenceLoading,
profileId,
profiles,
publishedWorks,
sensorWorks,
sessions.state,
workId,
workOptions,
]);
const selectProfile = (next: LaboratoryProfileId) => {
setProfileId(next);
if (next === "sensor-fusion") {
setWorkId("e28-local-surface");
const first = sensorWorks[0];
if (first) setWorkId(first.id);
return;
}
const first = publishedWorks[0];
@@ -1491,10 +1757,41 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
const selectWork = (next: LaboratoryWorkId) => {
setWorkId(next);
if (!next.startsWith("session:")) return;
void sessions.replay(next.slice("session:".length));
if (next.startsWith("session:")) {
void sessions.replay(next.slice("session:".length));
return;
}
if (next === "e29-camera-geometry" && e29SourceSession) {
void sessions.replay(e29SourceSession.id);
}
};
if (
evidenceLoading
|| sessions.state === "idle"
|| sessions.state === "loading"
) {
return (
<div className="laboratory-result-pending" role="status">
<span className="busy-indicator" aria-hidden="true" />
<strong>Ревизия лабораторных данных</strong>
<p>Проверяем артефакты, связанные исходные записи и доступность replay.</p>
</div>
);
}
if (!profiles.length) {
return (
<div className="laboratory-result-pending" role="status">
<Icon name="database" size={20} />
<strong>Подтверждённых лабораторных работ нет</strong>
<p>
{evidenceError ?? sessions.error ?? "Непроверенные и отсутствующие результаты скрыты."}
</p>
</div>
);
}
return (
<div className="lab-archive-workspace">
<LaboratorySelector
@@ -1524,12 +1821,15 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
<LaboratoryTask
title="LAB E28 · локальная модель поверхности L2.6"
description="Полная запись RAVNOVES00 воспроизводится через bounded shadow-контур. Модель оценивает локальную поверхность, наблюдаемые препятствия, временные скачки и ошибки предсказания без изменения исходного сенсора и без командного канала."
status="Диагностический replay"
status="Проверенные артефакты"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · LiDAR + pose · worker D` },
{ label: "Покрытие", value: "3 928 / 3 928 кадров" },
{
label: "Покрытие",
value: `${formatNumber(e28Model?.metrics.frames.valid ?? 0, 0)} / ${formatNumber(e28Model?.metrics.frames.total ?? 0, 0)} кадров`,
},
{ label: "Режим", value: "Recorded-source-paced shadow" },
{ label: "Полномочия", value: "Навигация и команды запрещены" },
{ label: "Артефакты", value: "Control plane · read-only · hash verified" },
]}
/>
<LidarQualityWorkspace
@@ -1538,8 +1838,19 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
onOpenObservation={() => props.navigation.openView("spatial-scene")}
/>
</>
) : workId === "e29-camera-geometry" ? (
<E29LaboratoryResult rigLabel={rigLabel} />
) : workId === "e29-camera-geometry" && e29Result && e29SourceSession ? (
<E29LaboratoryResult
props={props}
rigLabel={rigLabel}
result={e29Result}
sourceSession={e29SourceSession}
loading={sessions.replayingSessionId === e29SourceSession.id}
error={
sessions.failedSessionId === e29SourceSession.id
? sessions.error
: null
}
/>
) : selectedSession ? (
<PublishedLaboratoryResult
props={props}
@@ -1550,8 +1861,8 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
) : (
<div className="laboratory-result-pending">
<Icon name="database" size={20} />
<strong>В этом профиле пока нет опубликованных работ</strong>
<p>Работы появятся после публикации проверенного LAB-provenance.</p>
<strong>Работа не прошла ревизию</strong>
<p>Неподтверждённый результат скрыт из лабораторного каталога.</p>
</div>
)}
</div>