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;
}