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;
|
||||
}
|
||||
@@ -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} · цепочка 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
|
||||
|
||||
@@ -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 E31–E34 from separate read-only catalogs", async () => {
|
||||
test("decodes E31–E35 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 E31–E34 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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user