feat(perception): qualify E35 degradation recovery
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user