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
@@ -94,6 +94,7 @@ export interface AdvancedLaboratoryResults {
e32: E32LaboratoryResult | null;
e33: E33LaboratoryResult | null;
e34: E34TemporalLayerResult | null;
e35: E35DegradationRecoveryResult | null;
}
export class AdvancedLaboratoryContractError extends Error {
@@ -372,15 +373,20 @@ export async function fetchAdvancedLaboratoryResults({
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise<AdvancedLaboratoryResults> {
const [e31, e32, e33, e34] = await Promise.all([
const [e31, e32, e33, e34, e35] = await Promise.all([
fetchOne("/api/v1/laboratory/e31/results?limit=1", parseE31, fetcher, signal),
fetchOne("/api/v1/laboratory/e32/results?limit=1", parseE32, fetcher, signal),
fetchOne("/api/v1/laboratory/e33/results?limit=1", parseE33, fetcher, signal),
fetchE34TemporalLayerResult({ fetcher, signal }),
fetchE35DegradationRecoveryResult({ fetcher, signal }),
]);
return { e31, e32, e33, e34 };
return { e31, e32, e33, e34, e35 };
}
import {
fetchE34TemporalLayerResult,
type E34TemporalLayerResult,
} from "./e34TemporalLayer";
import {
fetchE35DegradationRecoveryResult,
type E35DegradationRecoveryResult,
} from "./e35DegradationRecovery";
@@ -0,0 +1,578 @@
import type {
E34OccupancyState,
E34OwnerKind,
E34Point3,
E34TemporalState,
} from "./e34TemporalLayer";
export type E35DegradationKind =
| "camera-loss"
| "lidar-loss"
| "pose-staleness"
| "delayed-frames"
| "bounded-drop"
| "timing-offset";
export type E35FaultPhase = "before" | "during" | "after";
export interface E35ScenarioMetrics {
scenarioId: E35DegradationKind;
kind: E35DegradationKind;
frameStart: number;
frameEnd: number;
framesProcessed: number;
injectedFrames: number;
droppedFrames: number;
hiddenSuccessFrames: number;
falseFreeRows: number;
semanticClaimsDuringCameraLoss: number;
metricRowsDuringLidarOrPoseLoss: number;
agreeClaimsDuringTimingOffset: number;
lateResultsReintroduced: number;
maximumCurrentComponentsDuringFault: number;
maximumHeldComponentsDuringFault: number;
expiredComponentsDuringFault: number;
recoveryFrameIndex: number;
recoverySeconds: number;
}
export interface E35TemporalComponent {
temporalId: number;
state: E34TemporalState;
occupancyState: E34OccupancyState;
ownerKind: E34OwnerKind;
semanticLabels: readonly string[];
centroidMapXyzM: E34Point3;
lastObservedAgeSeconds: number;
}
export interface E35RecoveryReviewFrame {
scenarioId: E35DegradationKind;
kind: E35DegradationKind;
frameIndex: number;
sessionSeconds: number;
faultPhase: E35FaultPhase;
action: string;
channels: Readonly<Record<string, string>>;
inputPointRows: number;
transformedPointRows: number;
layerState: "current" | "held" | "unknown";
counts: {
current: number;
held: number;
expired: number;
};
components: readonly E35TemporalComponent[];
cellCentersMapXyzM: readonly E34Point3[];
}
export interface E35RecoveryReviewScenario {
scenarioId: E35DegradationKind;
kind: E35DegradationKind;
frameStart: number;
frameEnd: number;
frames: readonly E35RecoveryReviewFrame[];
}
export interface E35DegradationRecoveryResult {
resultId: string;
createdAtUtc: string | null;
sourceSessionId: string;
status: "accepted-deterministic-degradation-recovery";
e32ResultId: string;
e33ResultId: string;
e34ResultId: string;
profileId: string;
pipelineId: string;
coordinateFrame: "map";
configuration: {
maximumRecoverySeconds: number;
scenarioCount: number;
};
metrics: {
sourceFrames: number;
variantCount: number;
variantFrameOutcomes: number;
injectionRecords: number;
variantFrameProcessingP95Ms: number;
buildElapsedMs: number;
};
scenarios: readonly E35ScenarioMetrics[];
reviewScenarios: readonly E35RecoveryReviewScenario[];
access: "read-only";
}
type LaboratoryFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
class E35DegradationRecoveryContractError extends Error {
constructor(message: string) {
super(message);
this.name = "E35DegradationRecoveryContractError";
}
}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new E35DegradationRecoveryContractError(
`${label}: ожидался объект.`,
);
}
return value as Record<string, unknown>;
}
function list(value: unknown, label: string): readonly unknown[] {
if (!Array.isArray(value)) {
throw new E35DegradationRecoveryContractError(
`${label}: ожидался массив.`,
);
}
return value;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new E35DegradationRecoveryContractError(
`${label}: ожидалась строка.`,
);
}
return value;
}
function optionalString(value: unknown, label: string): string | null {
return value === null ? null : stringValue(value, label);
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
throw new E35DegradationRecoveryContractError(
`${label}: ожидалось неотрицательное число.`,
);
}
return value;
}
function integerValue(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isSafeInteger(parsed)) {
throw new E35DegradationRecoveryContractError(
`${label}: ожидалось целое число.`,
);
}
return parsed;
}
function exactString<T extends string>(
value: unknown,
expected: T,
label: string,
): T {
if (value !== expected) {
throw new E35DegradationRecoveryContractError(
`${label}: неверное значение.`,
);
}
return expected;
}
function oneOf<T extends string>(
value: unknown,
expected: readonly T[],
label: string,
): T {
if (typeof value !== "string" || !expected.includes(value as T)) {
throw new E35DegradationRecoveryContractError(
`${label}: неверное значение.`,
);
}
return value as T;
}
function contentId(value: unknown, prefix: string, label: string): string {
const parsed = stringValue(value, label);
if (!new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(parsed)) {
throw new E35DegradationRecoveryContractError(
`${label}: неверный content id.`,
);
}
return parsed;
}
function point(value: unknown, label: string): E34Point3 {
const coordinates = list(value, label);
if (coordinates.length !== 3) {
throw new E35DegradationRecoveryContractError(
`${label}: ожидались XYZ.`,
);
}
const result = coordinates.map(
(coordinate, index) => {
if (typeof coordinate !== "number" || !Number.isFinite(coordinate)) {
throw new E35DegradationRecoveryContractError(
`${label}[${index}]: ожидалось число.`,
);
}
return coordinate;
},
);
return [result[0]!, result[1]!, result[2]!];
}
const DEGRADATION_KINDS = [
"camera-loss",
"lidar-loss",
"pose-staleness",
"delayed-frames",
"bounded-drop",
"timing-offset",
] as const;
function degradationKind(
value: unknown,
label: string,
): E35DegradationKind {
return oneOf(value, DEGRADATION_KINDS, label);
}
function parseComponent(
value: unknown,
label: string,
): E35TemporalComponent {
const item = record(value, label);
return {
temporalId: integerValue(item.temporal_id, `${label}.temporal_id`),
state: oneOf(
item.state,
["current", "held", "expired"] as const,
`${label}.state`,
),
occupancyState: oneOf(
item.occupancy_state,
["occupied", "unknown"] as const,
`${label}.occupancy_state`,
),
ownerKind: oneOf(
item.owner_kind,
["camera-track", "geometry-cluster"] as const,
`${label}.owner_kind`,
),
semanticLabels: list(
item.semantic_labels,
`${label}.semantic_labels`,
).map((entry, index) => stringValue(
entry,
`${label}.semantic_labels[${index}]`,
)),
centroidMapXyzM: point(
item.centroid_map_xyz_m,
`${label}.centroid_map_xyz_m`,
),
lastObservedAgeSeconds: numberValue(
item.last_observed_age_seconds,
`${label}.last_observed_age_seconds`,
),
};
}
function parseReviewFrame(
value: unknown,
label: string,
): E35RecoveryReviewFrame {
const item = record(value, label);
const counts = record(item.counts, `${label}.counts`);
const channels = record(item.channels, `${label}.channels`);
return {
scenarioId: degradationKind(
item.scenario_id,
`${label}.scenario_id`,
),
kind: degradationKind(item.kind, `${label}.kind`),
frameIndex: integerValue(item.frame_index, `${label}.frame_index`),
sessionSeconds: numberValue(
item.session_seconds,
`${label}.session_seconds`,
),
faultPhase: oneOf(
item.fault_phase,
["before", "during", "after"] as const,
`${label}.fault_phase`,
),
action: stringValue(item.action, `${label}.action`),
channels: Object.fromEntries(
Object.entries(channels).map(([key, entry]) => [
key,
stringValue(entry, `${label}.channels.${key}`),
]),
),
inputPointRows: integerValue(
item.input_point_rows,
`${label}.input_point_rows`,
),
transformedPointRows: integerValue(
item.transformed_point_rows,
`${label}.transformed_point_rows`,
),
layerState: oneOf(
item.layer_state,
["current", "held", "unknown"] as const,
`${label}.layer_state`,
),
counts: {
current: integerValue(counts.current, `${label}.counts.current`),
held: integerValue(counts.held, `${label}.counts.held`),
expired: integerValue(counts.expired, `${label}.counts.expired`),
},
components: list(item.components, `${label}.components`).map(
(entry, index) => parseComponent(
entry,
`${label}.components[${index}]`,
),
),
cellCentersMapXyzM: list(
item.cell_centers_map_xyz_m,
`${label}.cell_centers_map_xyz_m`,
).map((entry, index) => point(
entry,
`${label}.cell_centers_map_xyz_m[${index}]`,
)),
};
}
function parseReviewScenario(
value: unknown,
label: string,
): E35RecoveryReviewScenario {
const item = record(value, label);
const scenario = record(item.scenario, `${label}.scenario`);
return {
scenarioId: degradationKind(
scenario.scenario_id,
`${label}.scenario.scenario_id`,
),
kind: degradationKind(
scenario.kind,
`${label}.scenario.kind`,
),
frameStart: integerValue(
scenario.frame_start,
`${label}.scenario.frame_start`,
),
frameEnd: integerValue(
scenario.frame_end,
`${label}.scenario.frame_end`,
),
frames: list(item.frames, `${label}.frames`).map(
(entry, index) => parseReviewFrame(
entry,
`${label}.frames[${index}]`,
),
),
};
}
function parseScenarioMetrics(
value: unknown,
label: string,
): E35ScenarioMetrics {
const item = record(value, label);
const integer = (key: string) => integerValue(item[key], `${label}.${key}`);
return {
scenarioId: degradationKind(item.scenario_id, `${label}.scenario_id`),
kind: degradationKind(item.kind, `${label}.kind`),
frameStart: integer("frame_start"),
frameEnd: integer("frame_end"),
framesProcessed: integer("frames_processed"),
injectedFrames: integer("injected_frames"),
droppedFrames: integer("dropped_frames"),
hiddenSuccessFrames: integer("hidden_success_frames"),
falseFreeRows: integer("false_free_rows"),
semanticClaimsDuringCameraLoss: integer(
"semantic_claims_during_camera_loss",
),
metricRowsDuringLidarOrPoseLoss: integer(
"metric_rows_during_lidar_or_pose_loss",
),
agreeClaimsDuringTimingOffset: integer(
"agree_claims_during_timing_offset",
),
lateResultsReintroduced: integer("late_results_reintroduced"),
maximumCurrentComponentsDuringFault: integer(
"maximum_current_components_during_fault",
),
maximumHeldComponentsDuringFault: integer(
"maximum_held_components_during_fault",
),
expiredComponentsDuringFault: integer(
"expired_components_during_fault",
),
recoveryFrameIndex: integer("recovery_frame_index"),
recoverySeconds: numberValue(
item.recovery_seconds,
`${label}.recovery_seconds`,
),
};
}
function diagnosticAuthority(value: unknown, label: string): void {
const authority = record(value, label);
if (
authority.commands_enabled !== false
|| authority.navigation_or_safety_accepted !== false
) {
throw new E35DegradationRecoveryContractError(
`${label}: запрещённые полномочия.`,
);
}
}
function parseResult(value: unknown): E35DegradationRecoveryResult {
const item = record(value, "E35");
const configuration = record(item.configuration, "E35.configuration");
const metrics = record(item.metrics, "E35.metrics");
const review = record(item.review, "E35.review");
const acceptance = record(item.acceptance, "E35.acceptance");
diagnosticAuthority(item.authority, "E35.authority");
if (acceptance.accepted !== true) {
throw new E35DegradationRecoveryContractError(
"E35.acceptance: результат отклонён.",
);
}
exactString(
review.schema_version,
"missioncore.e35-recovery-review/v1",
"E35.review.schema_version",
);
return {
resultId: contentId(
item.result_id,
"e35-degradation-recovery",
"E35.result_id",
),
createdAtUtc: optionalString(item.created_at_utc, "E35.created_at_utc"),
sourceSessionId: stringValue(
item.source_session_id,
"E35.source_session_id",
),
status: exactString(
item.status,
"accepted-deterministic-degradation-recovery",
"E35.status",
),
e32ResultId: contentId(
item.e32_result_id,
"e32-track-geometry",
"E35.e32_result_id",
),
e33ResultId: contentId(
item.e33_result_id,
"e33-worker-shadow",
"E35.e33_result_id",
),
e34ResultId: contentId(
item.e34_result_id,
"e34-temporal-occupied",
"E35.e34_result_id",
),
profileId: stringValue(item.profile_id, "E35.profile_id"),
pipelineId: stringValue(item.pipeline_id, "E35.pipeline_id"),
coordinateFrame: exactString(
item.coordinate_frame,
"map",
"E35.coordinate_frame",
),
configuration: {
maximumRecoverySeconds: numberValue(
configuration.maximum_recovery_seconds,
"E35.configuration.maximum_recovery_seconds",
),
scenarioCount: integerValue(
configuration.scenario_count,
"E35.configuration.scenario_count",
),
},
metrics: {
sourceFrames: integerValue(
metrics.source_frames,
"E35.metrics.source_frames",
),
variantCount: integerValue(
metrics.variant_count,
"E35.metrics.variant_count",
),
variantFrameOutcomes: integerValue(
metrics.variant_frame_outcomes,
"E35.metrics.variant_frame_outcomes",
),
injectionRecords: integerValue(
metrics.injection_records,
"E35.metrics.injection_records",
),
variantFrameProcessingP95Ms: numberValue(
metrics.variant_frame_processing_p95_ms,
"E35.metrics.variant_frame_processing_p95_ms",
),
buildElapsedMs: numberValue(
metrics.build_elapsed_ms,
"E35.metrics.build_elapsed_ms",
),
},
scenarios: list(item.scenarios, "E35.scenarios").map(
(entry, index) => parseScenarioMetrics(
entry,
`E35.scenarios[${index}]`,
),
),
reviewScenarios: list(review.scenarios, "E35.review.scenarios").map(
(entry, index) => parseReviewScenario(
entry,
`E35.review.scenarios[${index}]`,
),
),
access: exactString(item.access, "read-only", "E35.access"),
};
}
export async function fetchE35DegradationRecoveryResult({
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise<E35DegradationRecoveryResult | null> {
const response = await fetcher(
"/api/v1/laboratory/e35/results?limit=1",
{
method: "GET",
headers: { Accept: "application/json" },
signal,
},
);
if (!response.ok) {
throw new E35DegradationRecoveryContractError(
`Каталог LAB E35 недоступен: HTTP ${response.status}.`,
);
}
const catalog = record(await response.json(), "Каталог LAB E35");
exactString(
catalog.schema_version,
"missioncore.laboratory-advanced-catalog/v1",
"Каталог LAB E35.schema_version",
);
if (typeof catalog.configured !== "boolean") {
throw new E35DegradationRecoveryContractError(
"Каталог LAB E35.configured: ожидался boolean.",
);
}
integerValue(catalog.candidate_total, "Каталог LAB E35.candidate_total");
integerValue(catalog.invalid_total, "Каталог LAB E35.invalid_total");
exactString(catalog.access, "read-only", "Каталог LAB E35.access");
const items = list(catalog.items, "Каталог LAB E35.items");
if (items.length > 1) {
throw new E35DegradationRecoveryContractError(
"Каталог LAB E35: нарушен limit=1.",
);
}
return items.length ? parseResult(items[0]) : null;
}
+1
View File
@@ -4,6 +4,7 @@
@import "./styles/laboratory.css";
@import "./styles/laboratory-reporting.css";
@import "./styles/e34-temporal-layer.css";
@import "./styles/e35-degradation-recovery.css";
@import "./styles/e30-human-review.css";
@import "./styles/spatial.css";
@import "./styles/device.css";
@@ -0,0 +1,83 @@
.e35-degradation-evidence {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.e35-degradation-evidence__selectors {
display: flex;
align-items: center;
gap: 0.45rem;
}
.e35-degradation-evidence__selectors .nodedc-select-anchor {
width: clamp(10rem, 20vw, 15rem);
}
.e35-degradation-evidence__timeline {
position: absolute;
z-index: 3;
top: 3.7rem;
right: 0.6rem;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.3rem;
max-width: calc(100% - 1.2rem);
}
.e35-degradation-evidence__timeline .nodedc-button {
background: var(--nodedc-floating-surface);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e35-degradation-evidence__telemetry {
position: absolute;
z-index: 3;
left: 0.6rem;
bottom: 0.6rem;
display: grid;
width: min(19rem, calc(100% - 1.2rem));
gap: 0.34rem;
margin: 0;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-floating-surface);
padding: 0.55rem 0.65rem;
color: var(--nodedc-text-secondary);
pointer-events: none;
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e35-degradation-evidence__telemetry > div {
display: grid;
gap: 0.1rem;
}
.e35-degradation-evidence__telemetry dt,
.e35-degradation-evidence__telemetry dd {
margin: 0;
font-size: 0.5rem;
}
.e35-degradation-evidence__telemetry dt {
color: var(--nodedc-text-muted);
}
.e35-degradation-evidence__telemetry dd {
overflow: hidden;
color: var(--nodedc-text-primary);
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 900px) {
.e35-degradation-evidence__timeline {
top: 4rem;
}
.e35-degradation-evidence__timeline .nodedc-button {
padding-inline: 0.55rem;
}
}
@@ -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} · цепочка E32E34` },
{
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,0860,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" ? "E31E34" : null,
advanced.status === "rejected" ? "E31E35" : null,
].filter(Boolean);
setEvidenceError(
failures.length
@@ -206,6 +206,99 @@ function e34() {
};
}
function e35() {
return {
result_id: `e35-degradation-recovery-${"5".repeat(64)}`,
created_at_utc: "2026-07-27T15:00:00Z",
source_session_id: "20260720T065719Z_viewer_live",
status: "accepted-deterministic-degradation-recovery",
e32_result_id: e32().result_id,
e33_result_id: e33().result_id,
e34_result_id: e34().result_id,
profile_id: "e35-six-channel-degradation-recovery/v1",
pipeline_id: "track-geometry/temporal-layer/degradation-recovery/v1",
coordinate_frame: "map",
configuration: {
maximum_recovery_seconds: 0.25,
scenario_count: 6,
},
metrics: {
source_frames: 4489,
variant_count: 6,
variant_frame_outcomes: 26934,
injection_records: 360,
variant_frame_processing_p95_ms: 1.08,
build_elapsed_ms: 18618,
},
scenarios: [{
scenario_id: "camera-loss",
kind: "camera-loss",
frame_start: 600,
frame_end: 659,
frames_processed: 4489,
injected_frames: 60,
dropped_frames: 0,
hidden_success_frames: 0,
false_free_rows: 0,
semantic_claims_during_camera_loss: 0,
metric_rows_during_lidar_or_pose_loss: 0,
agree_claims_during_timing_offset: 0,
late_results_reintroduced: 0,
maximum_current_components_during_fault: 13,
maximum_held_components_during_fault: 24,
expired_components_during_fault: 60,
recovery_frame_index: 660,
recovery_seconds: 0.086,
}],
review: {
schema_version: "missioncore.e35-recovery-review/v1",
result_id: `e35-degradation-recovery-${"5".repeat(64)}`,
scenarios: [{
scenario: {
schema_version: "missioncore.e35-degradation-scenario/v1",
scenario_id: "camera-loss",
kind: "camera-loss",
frame_start: 600,
frame_end: 659,
parameters: { drop_camera_observations: true },
},
frames: [{
scenario_id: "camera-loss",
kind: "camera-loss",
frame_index: 600,
source_frame_index: 600,
session_seconds: 95.4,
fault_phase: "during",
action: "camera-observations-removed",
channels: {
camera: "unavailable",
lidar: "available",
pose: "available",
delivery: "on-time",
},
input_point_rows: 300,
transformed_point_rows: 300,
layer_state: "current",
counts: { current: 5, held: 2, expired: 0 },
components: [{
temporal_id: 10,
state: "current",
occupancy_state: "occupied",
owner_kind: "geometry-cluster",
semantic_labels: [],
last_observed_age_seconds: 0,
centroid_map_xyz_m: [1, 2, 0.5],
}],
cell_centers_map_xyz_m: [[1, 2, 0.5]],
}],
}],
},
acceptance: { accepted: true },
authority,
access: "read-only",
};
}
before(async () => {
server = await createServer({
appType: "custom",
@@ -222,9 +315,9 @@ after(async () => {
await server?.close();
});
test("decodes E31E34 from separate read-only catalogs", async () => {
test("decodes E31E35 from separate read-only catalogs", async () => {
const requests = [];
const items = [e31(), e32(), e33(), e34()];
const items = [e31(), e32(), e33(), e34(), e35()];
const decoded = await fetchAdvancedLaboratoryResults({
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
@@ -241,11 +334,15 @@ test("decodes E31E34 from separate read-only catalogs", async () => {
assert.equal(decoded.e33.metrics.resultAgeMaxMs, 13.638);
assert.equal(decoded.e34.metrics.processedFrames, 4489);
assert.equal(decoded.e34.reviewFrames[0].components[0].state, "expired");
assert.equal(decoded.e35.metrics.variantFrameOutcomes, 26934);
assert.equal(decoded.e35.scenarios[0].recoverySeconds, 0.086);
assert.equal(decoded.e35.reviewScenarios[0].frames[0].faultPhase, "during");
assert.deepEqual(requests, [
{ input: "/api/v1/laboratory/e31/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e32/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e33/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e34/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e35/results?limit=1", method: "GET" },
]);
});
@@ -259,7 +356,8 @@ test("rejects authority escalation in an accepted-looking result", async () => {
fetcher: async (input) => new Response(JSON.stringify(catalog(
String(input).includes("/e31/") ? forged
: String(input).includes("/e32/") ? e32()
: String(input).includes("/e33/") ? e33() : e34(),
: String(input).includes("/e33/") ? e33()
: String(input).includes("/e34/") ? e34() : e35(),
)), { status: 200 }),
}),
AdvancedLaboratoryContractError,
@@ -26,6 +26,10 @@ const e34ResultUrl = new URL(
"../src/workspaces/laboratory/E34Result.tsx",
import.meta.url,
);
const e35ResultUrl = new URL(
"../src/workspaces/laboratory/E35Result.tsx",
import.meta.url,
);
const advancedLaboratoryResultUrl = new URL(
"../src/workspaces/laboratory/AdvancedLaboratoryResult.tsx",
import.meta.url,
@@ -161,6 +165,25 @@ test("E34 keeps temporal evidence inside the canonical LAB and viewer contracts"
assert.match(advancedSource, /<E34Result/);
});
test("E35 extends the canonical LAB with fault and recovery evidence", async () => {
const [e35Source, advancedSource] = await Promise.all([
readFile(e35ResultUrl, "utf8"),
readFile(advancedLaboratoryResultUrl, "utf8"),
]);
assert.match(e35Source, /<LaboratoryWorkTemplate/);
assert.match(e35Source, /<LaboratoryEvidenceViewer/);
assert.match(e35Source, /\{ value: "3d", label: "3D" \}/);
assert.match(e35Source, /\{ value: "plan", label: "План" \}/);
assert.match(e35Source, /Шесть неизменяемых полных replay-вариантов/);
assert.match(e35Source, /небезопасных утверждений —/);
assert.match(e35Source, /Следующий критический gate — A9\/E36/);
assert.match(e35Source, /SCENARIO_LABELS/);
assert.match(e35Source, /PHASE_LABELS/);
assert.match(advancedSource, /id: "e35-degradation-recovery"/);
assert.match(advancedSource, /<E35Result/);
});
test("the primary point-cloud viewer restores from fullscreen on Escape", async () => {
const workspacesSource = await readFile(workspacesUrl, "utf8");