feat(perception): qualify E35 degradation recovery

This commit is contained in:
DCCONSTRUCTIONS
2026-07-27 17:53:59 +03:00
parent 621084fcd6
commit b894945344
21 changed files with 3985 additions and 17 deletions
@@ -8,13 +8,15 @@ import { E31Result } from "./E31Result";
import { E32Result } from "./E32Result";
import { E33Result } from "./E33Result";
import { E34Result } from "./E34Result";
import { E35Result } from "./E35Result";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
export type AdvancedLaboratoryWorkId =
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
| "e34-temporal-layer";
| "e34-temporal-layer"
| "e35-degradation-recovery";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
@@ -28,6 +30,7 @@ export function isAdvancedLaboratoryWorkId(
|| value === "e32-track-geometry"
|| value === "e33-worker-shadow"
|| value === "e34-temporal-layer"
|| value === "e35-degradation-recovery"
);
}
@@ -60,6 +63,12 @@ export function advancedLaboratoryWorkOptions(
label: "LAB E34 · temporal occupied/unknown",
});
}
if (results.e35) {
options.push({
id: "e35-degradation-recovery",
label: "LAB E35 · degradation recovery",
});
}
return options;
}
@@ -97,6 +106,9 @@ export function AdvancedLaboratoryResult({
failedSessionId: string | null;
replayError: string | null;
}) {
if (workId === "e35-degradation-recovery" && results.e35) {
return <E35Result rigLabel={rigLabel} result={results.e35} />;
}
if (workId === "e34-temporal-layer" && results.e34) {
return <E34Result rigLabel={rigLabel} result={results.e34} />;
}
@@ -0,0 +1,340 @@
import { useMemo, useState } from "react";
import { Button, Select } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type {
E35DegradationKind,
E35DegradationRecoveryResult,
E35RecoveryReviewFrame,
E35RecoveryReviewScenario,
} from "../../core/laboratory/e35DegradationRecovery";
import type { E34TemporalReviewFrame } from "../../core/laboratory/e34TemporalLayer";
import { formatNumber } from "../../presentation";
import {
E34TemporalLayerScene,
type E34TemporalViewMode,
} from "./E34TemporalLayerScene";
const SCENARIO_LABELS: Record<E35DegradationKind, string> = {
"camera-loss": "Потеря камеры",
"lidar-loss": "Потеря LiDAR",
"pose-staleness": "Устаревшая поза",
"delayed-frames": "Просроченные кадры",
"bounded-drop": "Ограниченные пропуски",
"timing-offset": "Рассинхрон 250 мс",
};
const PHASE_LABELS = {
before: "До отказа",
during: "Во время отказа",
after: "После восстановления",
} as const;
function sceneFrame(frame: E35RecoveryReviewFrame): E34TemporalReviewFrame {
return {
frameIndex: frame.frameIndex,
sessionSeconds: frame.sessionSeconds,
sourceAvailable: frame.transformedPointRows > 0,
layerState: frame.layerState,
counts: frame.counts,
cellCentersMapXyzM: frame.cellCentersMapXyzM,
components: frame.components.map((component) => ({
temporalId: component.temporalId,
state: component.state,
occupancyState: component.occupancyState,
ownerKind: component.ownerKind,
centroidMapXyzM: component.centroidMapXyzM,
lastObservedAgeSeconds: component.lastObservedAgeSeconds,
associationReason: frame.action,
history: [],
})),
};
}
function channelSummary(frame: E35RecoveryReviewFrame): string {
return Object.entries(frame.channels)
.map(([channel, state]) => `${channel}: ${state}`)
.join(" · ");
}
function unsafeClaims(
result: E35DegradationRecoveryResult,
): number {
return result.scenarios.reduce((total, scenario) => (
total
+ scenario.hiddenSuccessFrames
+ scenario.falseFreeRows
+ scenario.semanticClaimsDuringCameraLoss
+ scenario.metricRowsDuringLidarOrPoseLoss
+ scenario.agreeClaimsDuringTimingOffset
+ scenario.lateResultsReintroduced
), 0);
}
function maximumRecovery(
result: E35DegradationRecoveryResult,
): number {
return Math.max(...result.scenarios.map(
(scenario) => scenario.recoverySeconds,
));
}
function defaultFrame(
scenario: E35RecoveryReviewScenario,
): E35RecoveryReviewFrame | null {
return scenario.frames.find((frame) => (
frame.faultPhase === "during"
&& (frame.counts.held > 0 || frame.counts.expired > 0)
)) ?? scenario.frames[0] ?? null;
}
function E35Evidence({
result,
}: {
result: E35DegradationRecoveryResult;
}) {
const initialScenario = result.reviewScenarios[0] ?? null;
const [scenarioId, setScenarioId] = useState<E35DegradationKind>(
initialScenario?.scenarioId ?? "camera-loss",
);
const scenario = result.reviewScenarios.find(
(item) => item.scenarioId === scenarioId,
) ?? initialScenario;
const initialFrame = scenario ? defaultFrame(scenario) : null;
const [frameIndex, setFrameIndex] = useState(initialFrame?.frameIndex ?? 0);
const frame = scenario?.frames.find(
(item) => item.frameIndex === frameIndex,
) ?? initialFrame;
const [mode, setMode] = useState<E34TemporalViewMode>("3d");
const [expanded, setExpanded] = useState(false);
const renderedFrame = useMemo(
() => frame ? sceneFrame(frame) : null,
[frame],
);
if (!scenario || !frame || !renderedFrame) {
return (
<div className="laboratory-result-pending" role="status">
Контрольные состояния деградации не опубликованы.
</div>
);
}
const selectScenario = (value: string) => {
const nextId = value as E35DegradationKind;
const nextScenario = result.reviewScenarios.find(
(item) => item.scenarioId === nextId,
);
setScenarioId(nextId);
setFrameIndex(defaultFrame(nextScenario ?? scenario)?.frameIndex ?? 0);
};
return (
<div className="e35-degradation-evidence">
<LaboratoryEvidenceViewer
label="Деградация и восстановление E35"
mode={mode}
modes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "План" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<div className="e35-degradation-evidence__selectors">
<Select
label="Сценарий отказа"
value={scenario.scenarioId}
options={result.reviewScenarios.map((item) => ({
value: item.scenarioId,
label: SCENARIO_LABELS[item.scenarioId],
}))}
variant="split"
menuWidth="anchor"
onChange={selectScenario}
/>
</div>
)}
overlay={(
<>
<div className="e35-degradation-evidence__timeline">
{scenario.frames.map((item, index) => (
<Button
key={item.frameIndex}
size="compact"
variant={item.frameIndex === frame.frameIndex
? "primary"
: "secondary"}
onClick={() => setFrameIndex(item.frameIndex)}
>
{index + 1}
{" · "}
{PHASE_LABELS[item.faultPhase]}
</Button>
))}
</div>
<dl className="e35-degradation-evidence__telemetry">
<div>
<dt>Сценарий / фаза</dt>
<dd>
{SCENARIO_LABELS[frame.kind]}
{" · "}
{PHASE_LABELS[frame.faultPhase]}
</dd>
</div>
<div>
<dt>Кадр / действие</dt>
<dd>
{formatNumber(frame.frameIndex, 0)}
{" · "}
{frame.action}
</dd>
</div>
<div>
<dt>Точки до / после</dt>
<dd>
{formatNumber(frame.inputPointRows, 0)}
{" / "}
{formatNumber(frame.transformedPointRows, 0)}
</dd>
</div>
<div>
<dt>Слой</dt>
<dd>
{frame.counts.current} current · {frame.counts.held} held · {frame.counts.expired} expired
</dd>
</div>
<div>
<dt>Каналы</dt>
<dd>{channelSummary(frame)}</dd>
</div>
</dl>
</>
)}
>
<E34TemporalLayerScene frame={renderedFrame} mode={mode} />
</LaboratoryEvidenceViewer>
</div>
);
}
export function E35Result({
rigLabel,
result,
}: {
rigLabel: string;
result: E35DegradationRecoveryResult;
}) {
const metrics = result.metrics;
const maximum = maximumRecovery(result);
const unsafe = unsafeClaims(result);
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E35 · деградация и восстановление"
description="Проверяли, теряет ли система только неподтверждённую способность при отказе камеры, LiDAR, позы или времени — и возвращается ли она в штатное состояние без скрытого успеха и ложного свободного пространства."
status="Безопасная деградация принята"
statusTone="success"
facts={[
{ label: "Источник", value: `${rigLabel} · цепочка E32–E34` },
{
label: "Объём проверки",
value: `${formatNumber(metrics.variantCount, 0)} × ${formatNumber(metrics.sourceFrames, 0)} кадров`,
},
{
label: "Инъекции отказов",
value: `${formatNumber(metrics.injectionRecords, 0)} записей · полный журнал`,
},
{ label: "Полномочия", value: "Диагностика · команды и safety выключены" },
]}
brief={{
question: "Станет ли отказ источника явным и ограниченным, или система продолжит публиковать неподтверждённую семантику, метрику либо свободное пространство?",
approach: "Шесть неизменяемых полных replay-вариантов получили по одному заранее объявленному отказу: потеря камеры, LiDAR, актуальной позы, просрочка, ограниченные пропуски и рассинхрон 250 мс. Каждый изменённый кадр и его восстановление записаны отдельно.",
principalResult: `Да, для этой записи и замороженного профиля. Все ${formatNumber(metrics.variantFrameOutcomes, 0)} исходов закрыты, небезопасных утверждений — ${unsafe}, максимальное восстановление — ${maximum.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с при gate ${result.configuration.maximumRecoverySeconds.toLocaleString("ru-RU")} с.`,
limitation: "Это source-scoped shadow qualification. Она не доказывает перенос на другой риг, свободное пространство, traversability, качество детектора, планирование или safety.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "E32 TrackGeometry → frozen E34 layer → E35 fault variants",
components: [
{
kind: "source",
name: "Принятая цепочка E32 / E33 / E34",
version: `${formatNumber(metrics.sourceFrames, 0)} кадров · map-frame`,
role: "неизменяемый TrackGeometry, runtime envelope и temporal layer",
identitySha256: null,
},
{
kind: "algorithm",
name: "Deterministic degradation transforms",
version: `${formatNumber(result.configuration.scenarioCount, 0)} сценариев · 60 кадров каждый`,
role: "явно удаляет неподтверждённый канал или разделяет несинхронные evidence claims",
identitySha256: null,
},
{
kind: "runtime",
name: "Independent frozen-layer replay",
version: `${metrics.variantFrameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс/variant-frame p95`,
role: "полный terminal accounting, injection journal, recovery и проверка upstream digest",
identitySha256: null,
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ДОКАЗАТЕЛЬСТВО ОТКАЗА И ВОССТАНОВЛЕНИЯ"
title="До отказа, потеря способности и возврат текущего evidence"
kind="diagnostic-model"
resizable
>
<E35Evidence result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Все шесть отказов становятся явными и восстанавливаются на следующем кадре"
status="14 / 14 gate"
statusTone="success"
metrics={[
{
label: "Полное покрытие",
value: formatNumber(metrics.variantFrameOutcomes, 0),
hint: `${formatNumber(metrics.variantCount, 0)} полных replay-варианта`,
},
{
label: "Макс. восстановление",
value: `${maximum.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`,
hint: `gate ≤ ${result.configuration.maximumRecoverySeconds.toLocaleString("ru-RU")} с`,
},
{
label: "Небезопасные claims",
value: formatNumber(unsafe, 0),
hint: "нет hidden success, false free или late return",
},
{
label: "Журнал инъекций",
value: formatNumber(metrics.injectionRecords, 0),
hint: "каждое изменение имеет исходный и итоговый digest",
},
]}
conclusion={{
proved: "На RAVNOVES00 потеря камеры, LiDAR, актуальной позы и времени явно понижает доступную способность: evidence становится geometry-only, camera-only, held/expired unknown или отбрасывается. Номинальное current evidence возвращается за 0,086–0,102 с.",
notProved: "Работа не доказывает переносимость на другую запись или mount, detector accuracy, свободное пространство, traversability, planner input, навигацию или safety.",
decision: "E35 принимается и закрывает A8. Следующий критический gate — A9/E36: аудит каталога и frozen-profile replay на подходящем втором mounted real source без retuning.",
}}
/>
)}
/>
);
}
@@ -67,6 +67,7 @@ const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
e32: null,
e33: null,
e34: null,
e35: null,
};
function digestFromContentId(value: string | null | undefined): string | null {
@@ -609,7 +610,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
e28.status === "rejected" ? "E28" : null,
e29.status === "rejected" ? "E29" : null,
e30.status === "rejected" ? "E30" : null,
advanced.status === "rejected" ? "E31–E34" : null,
advanced.status === "rejected" ? "E31–E35" : null,
].filter(Boolean);
setEvidenceError(
failures.length