diff --git a/apps/control-station/src/components/laboratory/LaboratoryPresentation.tsx b/apps/control-station/src/components/laboratory/LaboratoryPresentation.tsx
index 7a138e8..c785188 100644
--- a/apps/control-station/src/components/laboratory/LaboratoryPresentation.tsx
+++ b/apps/control-station/src/components/laboratory/LaboratoryPresentation.tsx
@@ -263,6 +263,24 @@ export function LaboratoryConclusion({
);
}
+export function LaboratoryMetricGrid({
+ metrics,
+}: {
+ metrics: readonly LaboratoryResultMetric[];
+}) {
+ return (
+
+ {metrics.map((metric) => (
+
+ {metric.label}
+ {metric.value}
+ {metric.hint}
+
+ ))}
+
+ );
+}
+
export function LaboratoryResultSummary({
title,
status,
@@ -285,15 +303,7 @@ export function LaboratoryResultSummary({
{status}
-
- {metrics.map((metric) => (
-
- {metric.label}
- {metric.value}
- {metric.hint}
-
- ))}
-
+
);
diff --git a/apps/control-station/src/core/laboratory/advancedResults.ts b/apps/control-station/src/core/laboratory/advancedResults.ts
index 1384c1a..23c06bc 100644
--- a/apps/control-station/src/core/laboratory/advancedResults.ts
+++ b/apps/control-station/src/core/laboratory/advancedResults.ts
@@ -1,3 +1,17 @@
+import {
+ fetchE34TemporalLayerResult,
+ type E34TemporalLayerResult,
+} from "./e34TemporalLayer";
+import {
+ fetchE35DegradationRecoveryResult,
+ type E35DegradationRecoveryResult,
+} from "./e35DegradationRecovery";
+import {
+ fetchE40ProductGateResult,
+ type E40PerceptionProductGateResult,
+} from "./e40ProductGate";
+import { settledCatalogValue } from "./catalogTransport";
+
export interface E31LaboratoryResult {
resultId: string;
createdAtUtc: string | null;
@@ -179,7 +193,7 @@ export interface E38PerceptionBaselineResult {
access: "read-only";
}
-export interface E39DevelopmentDimensionMetric {
+export interface DevelopmentDimensionMetric {
correct: number;
incorrect: number;
total: number;
@@ -188,6 +202,8 @@ export interface E39DevelopmentDimensionMetric {
passed: boolean;
}
+export type E39DevelopmentDimensionMetric = DevelopmentDimensionMetric;
+
export interface E39PerceptionRefinementResult {
resultId: string;
createdAtUtc: string | null;
@@ -205,9 +221,9 @@ export interface E39PerceptionRefinementResult {
validationLabelsUsed: false;
passed: boolean;
dimensions: {
- presence: E39DevelopmentDimensionMetric;
- geometryAssociation: E39DevelopmentDimensionMetric;
- freshness: E39DevelopmentDimensionMetric;
+ presence: DevelopmentDimensionMetric;
+ geometryAssociation: DevelopmentDimensionMetric;
+ freshness: DevelopmentDimensionMetric;
};
};
metrics: E38PerceptionBaselineResult["metrics"];
@@ -230,6 +246,7 @@ export interface AdvancedLaboratoryResults {
e37: E37AcceptanceContractResult | null;
e38: E38PerceptionBaselineResult | null;
e39: E39PerceptionRefinementResult | null;
+ e40: E40PerceptionProductGateResult | null;
}
export class AdvancedLaboratoryContractError extends Error {
@@ -778,10 +795,10 @@ function parseE38(value: unknown): E38PerceptionBaselineResult {
};
}
-function parseE39DevelopmentDimension(
+function parseDevelopmentDimension(
value: unknown,
label: string,
-): E39DevelopmentDimensionMetric {
+): DevelopmentDimensionMetric {
const source = record(value, label);
return {
correct: integerValue(source.correct, `${label}.correct`),
@@ -859,15 +876,15 @@ function parseE39(value: unknown): E39PerceptionRefinementResult {
"E39.development_cross_validation.passed",
),
dimensions: {
- presence: parseE39DevelopmentDimension(
+ presence: parseDevelopmentDimension(
developmentDimensions.presence,
"E39.development_cross_validation.dimensions.presence",
),
- geometryAssociation: parseE39DevelopmentDimension(
+ geometryAssociation: parseDevelopmentDimension(
developmentDimensions.geometry_association,
"E39.development_cross_validation.dimensions.geometry_association",
),
- freshness: parseE39DevelopmentDimension(
+ freshness: parseDevelopmentDimension(
developmentDimensions.freshness,
"E39.development_cross_validation.dimensions.freshness",
),
@@ -951,7 +968,7 @@ export async function fetchAdvancedLaboratoryResults({
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise {
- const [e31, e32, e33, e34, e35, e37, e38, e39] = await Promise.all([
+ const settled = await Promise.allSettled([
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),
@@ -960,14 +977,16 @@ export async function fetchAdvancedLaboratoryResults({
fetchOne("/api/v1/laboratory/e37/results?limit=1", parseE37, fetcher, signal),
fetchOne("/api/v1/laboratory/e38/results?limit=1", parseE38, fetcher, signal),
fetchOne("/api/v1/laboratory/e39/results?limit=1", parseE39, fetcher, signal),
+ fetchE40ProductGateResult({ fetcher, signal }),
]);
- return { e31, e32, e33, e34, e35, e37, e38, e39 };
+ const e31 = settledCatalogValue(settled[0]);
+ const e32 = settledCatalogValue(settled[1]);
+ const e33 = settledCatalogValue(settled[2]);
+ const e34 = settledCatalogValue(settled[3]);
+ const e35 = settledCatalogValue(settled[4]);
+ const e37 = settledCatalogValue(settled[5]);
+ const e38 = settledCatalogValue(settled[6]);
+ const e39 = settledCatalogValue(settled[7]);
+ const e40 = settledCatalogValue(settled[8]);
+ return { e31, e32, e33, e34, e35, e37, e38, e39, e40 };
}
-import {
- fetchE34TemporalLayerResult,
- type E34TemporalLayerResult,
-} from "./e34TemporalLayer";
-import {
- fetchE35DegradationRecoveryResult,
- type E35DegradationRecoveryResult,
-} from "./e35DegradationRecovery";
diff --git a/apps/control-station/src/core/laboratory/catalogTransport.ts b/apps/control-station/src/core/laboratory/catalogTransport.ts
new file mode 100644
index 0000000..332e198
--- /dev/null
+++ b/apps/control-station/src/core/laboratory/catalogTransport.ts
@@ -0,0 +1,17 @@
+export function settledCatalogValue(
+ result: PromiseSettledResult,
+): T | null {
+ if (result.status === "fulfilled") {
+ return result.value;
+ }
+ const reason = result.reason;
+ const isAbort = reason instanceof Error && reason.name === "AbortError";
+ const isHttpTransport = (
+ reason instanceof Error
+ && /Каталог LAB.*недоступен: HTTP \d+\./.test(reason.message)
+ );
+ if (!isAbort && (reason instanceof TypeError || isHttpTransport)) {
+ return null;
+ }
+ throw reason;
+}
diff --git a/apps/control-station/src/core/laboratory/e40ProductGate.ts b/apps/control-station/src/core/laboratory/e40ProductGate.ts
new file mode 100644
index 0000000..b3b4208
--- /dev/null
+++ b/apps/control-station/src/core/laboratory/e40ProductGate.ts
@@ -0,0 +1,458 @@
+export interface E40DevelopmentDimensionMetric {
+ correct: number;
+ incorrect: number;
+ total: number;
+ accuracy: number;
+ target: number;
+ passed: boolean;
+}
+
+export interface E40DimensionMetric extends E40DevelopmentDimensionMetric {
+ byStratum: Readonly>;
+ confusion: readonly {
+ reference: string;
+ prediction: string;
+ count: number;
+ }[];
+}
+
+export interface E40DevelopmentProtocol {
+ items: number;
+ foldSizes: Readonly>;
+ passed: boolean;
+ dimensions: {
+ presence: E40DevelopmentDimensionMetric;
+ geometryAssociation: E40DevelopmentDimensionMetric;
+ freshness: E40DevelopmentDimensionMetric;
+ };
+}
+
+export interface E40PerceptionProductGateResult {
+ resultId: string;
+ createdAtUtc: string | null;
+ sourceSessionId: string;
+ sourceDisplayName: string;
+ status: "measured-leakage-resistant-product-gate";
+ profileId: string;
+ workerNode: string;
+ qualityGatePassed: boolean;
+ developmentCrossValidation: {
+ strategy: string;
+ seed: string;
+ folds: number;
+ items: number;
+ validationLabelsUsed: false;
+ passed: boolean;
+ protocols: {
+ contiguousSourceTime: E40DevelopmentProtocol;
+ wholeTrackOrSceneWindow: E40DevelopmentProtocol;
+ };
+ };
+ metrics: {
+ developmentItems: number;
+ validationItems: number;
+ terminalOutcomes: number;
+ accountingFraction: number;
+ falseFreeClaims: number;
+ highSeverityFailures: number;
+ dimensions: {
+ presence: E40DimensionMetric;
+ geometryAssociation: E40DimensionMetric;
+ freshness: E40DimensionMetric;
+ };
+ };
+ blockingChecks: readonly string[];
+ method: {
+ summary: string;
+ selection: string;
+ dimensionProjection: string;
+ };
+ limitations: readonly string[];
+ access: "read-only";
+}
+
+export class E40ProductGateContractError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "E40ProductGateContractError";
+ }
+}
+
+type LaboratoryFetch = (
+ input: RequestInfo | URL,
+ init?: RequestInit,
+) => Promise;
+
+function record(value: unknown, label: string): Record {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ throw new E40ProductGateContractError(`${label}: ожидался объект.`);
+ }
+ return value as Record;
+}
+
+function stringValue(value: unknown, label: string): string {
+ if (typeof value !== "string" || !value.trim()) {
+ throw new E40ProductGateContractError(`${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 E40ProductGateContractError(`${label}: ожидалось число.`);
+ }
+ return value;
+}
+
+function integerValue(value: unknown, label: string): number {
+ const parsed = numberValue(value, label);
+ if (!Number.isSafeInteger(parsed)) {
+ throw new E40ProductGateContractError(`${label}: ожидалось целое число.`);
+ }
+ return parsed;
+}
+
+function booleanValue(value: unknown, label: string): boolean {
+ if (typeof value !== "boolean") {
+ throw new E40ProductGateContractError(`${label}: ожидался boolean.`);
+ }
+ return value;
+}
+
+function falseValue(value: unknown, label: string): false {
+ if (value !== false) {
+ throw new E40ProductGateContractError(`${label}: ожидалось false.`);
+ }
+ return false;
+}
+
+function exactString(
+ value: unknown,
+ expected: T,
+ label: string,
+): T {
+ if (value !== expected) {
+ throw new E40ProductGateContractError(`${label}: неверное значение.`);
+ }
+ return expected;
+}
+
+function strings(value: unknown, label: string): readonly string[] {
+ if (!Array.isArray(value)) {
+ throw new E40ProductGateContractError(`${label}: ожидался массив.`);
+ }
+ return value.map((item, index) => stringValue(item, `${label}[${index}]`));
+}
+
+function numberRecord(
+ value: unknown,
+ label: string,
+): Readonly> {
+ const source = record(value, label);
+ return Object.fromEntries(
+ Object.entries(source).map(([key, item]) => [
+ key,
+ integerValue(item, `${label}.${key}`),
+ ]),
+ );
+}
+
+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 E40ProductGateContractError(`${label}: неверный content id.`);
+ }
+ return parsed;
+}
+
+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 E40ProductGateContractError(`${label}: запрещённые полномочия.`);
+ }
+}
+
+function parseDevelopmentDimension(
+ value: unknown,
+ label: string,
+): E40DevelopmentDimensionMetric {
+ const dimension = record(value, label);
+ return {
+ correct: integerValue(dimension.correct, `${label}.correct`),
+ incorrect: integerValue(dimension.incorrect, `${label}.incorrect`),
+ total: integerValue(dimension.total, `${label}.total`),
+ accuracy: numberValue(dimension.accuracy, `${label}.accuracy`),
+ target: numberValue(dimension.target, `${label}.target`),
+ passed: booleanValue(dimension.passed, `${label}.passed`),
+ };
+}
+
+function parseDimension(value: unknown, label: string): E40DimensionMetric {
+ const dimension = record(value, label);
+ const byStratum = record(dimension.by_stratum, `${label}.by_stratum`);
+ const confusion = dimension.confusion;
+ if (!Array.isArray(confusion)) {
+ throw new E40ProductGateContractError(
+ `${label}.confusion: ожидался массив.`,
+ );
+ }
+ return {
+ ...parseDevelopmentDimension(value, label),
+ byStratum: Object.fromEntries(
+ Object.entries(byStratum).map(([stratum, raw]) => {
+ const item = record(raw, `${label}.by_stratum.${stratum}`);
+ return [stratum, {
+ correct: integerValue(
+ item.correct,
+ `${label}.by_stratum.${stratum}.correct`,
+ ),
+ incorrect: integerValue(
+ item.incorrect,
+ `${label}.by_stratum.${stratum}.incorrect`,
+ ),
+ total: integerValue(
+ item.total,
+ `${label}.by_stratum.${stratum}.total`,
+ ),
+ accuracy: numberValue(
+ item.accuracy,
+ `${label}.by_stratum.${stratum}.accuracy`,
+ ),
+ }];
+ }),
+ ),
+ confusion: confusion.map((raw, index) => {
+ const item = record(raw, `${label}.confusion[${index}]`);
+ return {
+ reference: stringValue(
+ item.reference,
+ `${label}.confusion[${index}].reference`,
+ ),
+ prediction: stringValue(
+ item.prediction,
+ `${label}.confusion[${index}].prediction`,
+ ),
+ count: integerValue(
+ item.count,
+ `${label}.confusion[${index}].count`,
+ ),
+ };
+ }),
+ };
+}
+
+function parseDevelopmentProtocol(
+ value: unknown,
+ label: string,
+): E40DevelopmentProtocol {
+ const protocol = record(value, label);
+ const dimensions = record(protocol.dimensions, `${label}.dimensions`);
+ return {
+ items: integerValue(protocol.items, `${label}.items`),
+ foldSizes: numberRecord(protocol.fold_sizes, `${label}.fold_sizes`),
+ passed: booleanValue(protocol.passed, `${label}.passed`),
+ dimensions: {
+ presence: parseDevelopmentDimension(
+ dimensions.presence,
+ `${label}.dimensions.presence`,
+ ),
+ geometryAssociation: parseDevelopmentDimension(
+ dimensions.geometry_association,
+ `${label}.dimensions.geometry_association`,
+ ),
+ freshness: parseDevelopmentDimension(
+ dimensions.freshness,
+ `${label}.dimensions.freshness`,
+ ),
+ },
+ };
+}
+
+function parseResult(value: unknown): E40PerceptionProductGateResult {
+ const item = record(value, "E40");
+ const metrics = record(item.metrics, "E40.metrics");
+ const dimensions = record(metrics.dimensions, "E40.metrics.dimensions");
+ const development = record(
+ item.development_cross_validation,
+ "E40.development_cross_validation",
+ );
+ const protocols = record(
+ development.protocols,
+ "E40.development_cross_validation.protocols",
+ );
+ const method = record(item.method, "E40.method");
+ diagnosticAuthority(item.authority, "E40.authority");
+ return {
+ resultId: contentId(
+ item.result_id,
+ "e40-perception-product-gate",
+ "E40.result_id",
+ ),
+ createdAtUtc: optionalString(item.created_at_utc, "E40.created_at_utc"),
+ sourceSessionId: stringValue(
+ item.source_session_id,
+ "E40.source_session_id",
+ ),
+ sourceDisplayName: stringValue(
+ item.source_display_name,
+ "E40.source_display_name",
+ ),
+ status: exactString(
+ item.status,
+ "measured-leakage-resistant-product-gate",
+ "E40.status",
+ ),
+ profileId: stringValue(item.profile_id, "E40.profile_id"),
+ workerNode: stringValue(item.worker_node, "E40.worker_node"),
+ qualityGatePassed: booleanValue(
+ item.quality_gate_passed,
+ "E40.quality_gate_passed",
+ ),
+ developmentCrossValidation: {
+ strategy: stringValue(
+ development.strategy,
+ "E40.development_cross_validation.strategy",
+ ),
+ seed: stringValue(
+ development.seed,
+ "E40.development_cross_validation.seed",
+ ),
+ folds: integerValue(
+ development.folds,
+ "E40.development_cross_validation.folds",
+ ),
+ items: integerValue(
+ development.items,
+ "E40.development_cross_validation.items",
+ ),
+ validationLabelsUsed: falseValue(
+ development.validation_labels_used,
+ "E40.development_cross_validation.validation_labels_used",
+ ),
+ passed: booleanValue(
+ development.passed,
+ "E40.development_cross_validation.passed",
+ ),
+ protocols: {
+ contiguousSourceTime: parseDevelopmentProtocol(
+ protocols["contiguous-source-time-five-fold"],
+ "E40.development_cross_validation.protocols.contiguous-source-time-five-fold",
+ ),
+ wholeTrackOrSceneWindow: parseDevelopmentProtocol(
+ protocols["whole-track-or-scene-window-five-fold"],
+ "E40.development_cross_validation.protocols.whole-track-or-scene-window-five-fold",
+ ),
+ },
+ },
+ metrics: {
+ developmentItems: integerValue(
+ metrics.development_items,
+ "E40.metrics.development_items",
+ ),
+ validationItems: integerValue(
+ metrics.validation_items,
+ "E40.metrics.validation_items",
+ ),
+ terminalOutcomes: integerValue(
+ metrics.terminal_outcomes,
+ "E40.metrics.terminal_outcomes",
+ ),
+ accountingFraction: numberValue(
+ metrics.accounting_fraction,
+ "E40.metrics.accounting_fraction",
+ ),
+ falseFreeClaims: integerValue(
+ metrics.false_free_claims,
+ "E40.metrics.false_free_claims",
+ ),
+ highSeverityFailures: integerValue(
+ metrics.high_severity_failures,
+ "E40.metrics.high_severity_failures",
+ ),
+ dimensions: {
+ presence: parseDimension(
+ dimensions.presence,
+ "E40.metrics.dimensions.presence",
+ ),
+ geometryAssociation: parseDimension(
+ dimensions.geometry_association,
+ "E40.metrics.dimensions.geometry_association",
+ ),
+ freshness: parseDimension(
+ dimensions.freshness,
+ "E40.metrics.dimensions.freshness",
+ ),
+ },
+ },
+ blockingChecks: strings(item.blocking_checks, "E40.blocking_checks"),
+ method: {
+ summary: stringValue(method.summary, "E40.method.summary"),
+ selection: stringValue(method.selection, "E40.method.selection"),
+ dimensionProjection: stringValue(
+ method.dimension_projection,
+ "E40.method.dimension_projection",
+ ),
+ },
+ limitations: strings(item.limitations, "E40.limitations"),
+ access: exactString(item.access, "read-only", "E40.access"),
+ };
+}
+
+function parseCatalog(payload: unknown): E40PerceptionProductGateResult | null {
+ const catalog = record(payload, "Каталог LAB E40");
+ exactString(
+ catalog.schema_version,
+ "missioncore.laboratory-advanced-catalog/v1",
+ "Каталог LAB E40.schema_version",
+ );
+ booleanValue(catalog.configured, "Каталог LAB E40.configured");
+ integerValue(catalog.candidate_total, "Каталог LAB E40.candidate_total");
+ integerValue(catalog.invalid_total, "Каталог LAB E40.invalid_total");
+ exactString(catalog.access, "read-only", "Каталог LAB E40.access");
+ if (!Array.isArray(catalog.items)) {
+ throw new E40ProductGateContractError(
+ "Каталог LAB E40.items: ожидался массив.",
+ );
+ }
+ if (catalog.items.length > 1) {
+ throw new E40ProductGateContractError(
+ "Каталог LAB E40.items: нарушен limit=1.",
+ );
+ }
+ return catalog.items.length ? parseResult(catalog.items[0]) : null;
+}
+
+export async function fetchE40ProductGateResult({
+ fetcher = fetch,
+ signal,
+}: {
+ fetcher?: LaboratoryFetch;
+ signal?: AbortSignal;
+} = {}): Promise {
+ const response = await fetcher(
+ "/api/v1/laboratory/e40/results?limit=1",
+ {
+ method: "GET",
+ headers: { Accept: "application/json" },
+ signal,
+ },
+ );
+ if (!response.ok) {
+ throw new E40ProductGateContractError(
+ `Каталог LAB E40 недоступен: HTTP ${response.status}.`,
+ );
+ }
+ return parseCatalog(await response.json());
+}
diff --git a/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx b/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx
index 3f40029..a7eeedd 100644
--- a/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx
+++ b/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx
@@ -12,6 +12,7 @@ import { E35Result } from "./E35Result";
import { E37Result } from "./E37Result";
import { E38Result } from "./E38Result";
import { E39Result } from "./E39Result";
+import { E40Result } from "./E40Result";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
export type AdvancedLaboratoryWorkId =
@@ -22,7 +23,8 @@ export type AdvancedLaboratoryWorkId =
| "e35-degradation-recovery"
| "e37-ravnoves-acceptance"
| "e38-perception-baseline"
- | "e39-perception-refinement";
+ | "e39-perception-refinement"
+ | "e40-perception-product-gate";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType;
@@ -40,6 +42,7 @@ export function isAdvancedLaboratoryWorkId(
|| value === "e37-ravnoves-acceptance"
|| value === "e38-perception-baseline"
|| value === "e39-perception-refinement"
+ || value === "e40-perception-product-gate"
);
}
@@ -96,6 +99,12 @@ export function advancedLaboratoryWorkOptions(
label: "LAB E39 · perception refinement R1",
});
}
+ if (results.e40) {
+ options.push({
+ id: "e40-perception-product-gate",
+ label: "LAB E40 · leakage-resistant product gate",
+ });
+ }
return options;
}
@@ -133,6 +142,9 @@ export function AdvancedLaboratoryResult({
failedSessionId: string | null;
replayError: string | null;
}) {
+ if (workId === "e40-perception-product-gate" && results.e40) {
+ return ;
+ }
if (workId === "e39-perception-refinement" && results.e39) {
return ;
}
diff --git a/apps/control-station/src/workspaces/laboratory/E40Result.tsx b/apps/control-station/src/workspaces/laboratory/E40Result.tsx
new file mode 100644
index 0000000..c1a15cc
--- /dev/null
+++ b/apps/control-station/src/workspaces/laboratory/E40Result.tsx
@@ -0,0 +1,180 @@
+import {
+ LaboratoryEvidence,
+ LaboratoryMetricGrid,
+ LaboratoryResultSummary,
+ LaboratorySummary,
+ LaboratoryWorkTemplate,
+} from "../../components/laboratory/LaboratoryPresentation";
+import type {
+ E40PerceptionProductGateResult,
+} from "../../core/laboratory/e40ProductGate";
+import { formatNumber } from "../../presentation";
+
+function percent(value: number): string {
+ return `${(value * 100).toLocaleString("ru-RU", {
+ maximumFractionDigits: 1,
+ })}%`;
+}
+
+export function E40Result({
+ rigLabel,
+ result,
+}: {
+ rigLabel: string;
+ result: E40PerceptionProductGateResult;
+}) {
+ const metrics = result.metrics;
+ const dimensions = metrics.dimensions;
+ const contiguous = (
+ result.developmentCrossValidation.protocols.contiguousSourceTime.dimensions
+ );
+ const grouped = (
+ result.developmentCrossValidation.protocols.wholeTrackOrSceneWindow.dimensions
+ );
+ const gateLabel = result.qualityGatePassed
+ ? "RAVNOVES00 product gate пройден"
+ : "RAVNOVES00 product gate не пройден";
+ return (
+
+ )}
+ evidence={(
+
+
+
+ )}
+ result={(
+
+ )}
+ />
+ );
+}
diff --git a/apps/control-station/src/workspaces/laboratory/LaboratoryArchiveWorkspace.tsx b/apps/control-station/src/workspaces/laboratory/LaboratoryArchiveWorkspace.tsx
index 80fc160..f27d9c3 100644
--- a/apps/control-station/src/workspaces/laboratory/LaboratoryArchiveWorkspace.tsx
+++ b/apps/control-station/src/workspaces/laboratory/LaboratoryArchiveWorkspace.tsx
@@ -72,6 +72,7 @@ const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
e37: null,
e38: null,
e39: null,
+ e40: null,
};
function laboratoryWorkOrdinal(value: string): number {
@@ -625,7 +626,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–E35" : null,
+ advanced.status === "rejected" ? "E31–E40" : null,
].filter(Boolean);
setEvidenceError(
failures.length
diff --git a/apps/control-station/test/advancedLaboratoryResults.test.mjs b/apps/control-station/test/advancedLaboratoryResults.test.mjs
index 32b05ec..a2dce1b 100644
--- a/apps/control-station/test/advancedLaboratoryResults.test.mjs
+++ b/apps/control-station/test/advancedLaboratoryResults.test.mjs
@@ -526,6 +526,107 @@ function e39() {
};
}
+function e40DevelopmentProtocol({
+ accuracy,
+ correct,
+ incorrect,
+}) {
+ return {
+ items: 340,
+ fold_sizes: {
+ 0: 68,
+ 1: 68,
+ 2: 68,
+ 3: 68,
+ 4: 68,
+ },
+ passed: true,
+ dimensions: {
+ presence: e39DevelopmentDimension({ accuracy, correct, incorrect }),
+ geometry_association: e39DevelopmentDimension({
+ accuracy,
+ correct,
+ incorrect,
+ }),
+ freshness: e39DevelopmentDimension({
+ accuracy: 0.967647,
+ correct: 329,
+ incorrect: 11,
+ }),
+ },
+ };
+}
+
+function e40() {
+ return {
+ result_id: `e40-perception-product-gate-${"a".repeat(64)}`,
+ created_at_utc: "2026-07-28T08:30:00Z",
+ source_session_id: "20260720T065719Z_viewer_live",
+ source_display_name: "RAVNOVES00",
+ status: "measured-leakage-resistant-product-gate",
+ profile_id: "e40-ravnoves00-leakage-resistant-product-gate/v1",
+ worker_node: "DESKTOP-OPJ8J04",
+ quality_gate_passed: true,
+ development_cross_validation: {
+ strategy: "dual-leakage-resistant-development-five-fold",
+ seed: "e40-development-cv-v1",
+ folds: 5,
+ items: 340,
+ validation_labels_used: false,
+ passed: true,
+ protocols: {
+ "contiguous-source-time-five-fold": e40DevelopmentProtocol({
+ accuracy: 0.908824,
+ correct: 309,
+ incorrect: 31,
+ }),
+ "whole-track-or-scene-window-five-fold": e40DevelopmentProtocol({
+ accuracy: 0.920588,
+ correct: 313,
+ incorrect: 27,
+ }),
+ },
+ },
+ metrics: {
+ development_items: 340,
+ validation_items: 146,
+ terminal_outcomes: 146,
+ accounting_fraction: 1,
+ false_free_claims: 0,
+ high_severity_failures: 0,
+ dimensions: {
+ presence: e38Dimension({
+ accuracy: 0.90411,
+ correct: 132,
+ incorrect: 14,
+ passed: true,
+ }),
+ geometry_association: e38Dimension({
+ accuracy: 0.90411,
+ correct: 132,
+ incorrect: 14,
+ passed: true,
+ }),
+ freshness: e38Dimension({
+ accuracy: 0.958904,
+ correct: 140,
+ incorrect: 6,
+ passed: true,
+ }),
+ },
+ },
+ blocking_checks: [],
+ method: {
+ summary: "conservative policy plus camera-only softmax",
+ selection: "dual grouped development cross-validation",
+ dimension_projection: "presence plus immutable stratum",
+ },
+ limitations: ["RAVNOVES00 source scoped"],
+ authority,
+ access: "read-only",
+ };
+}
+
before(async () => {
server = await createServer({
appType: "custom",
@@ -542,9 +643,11 @@ after(async () => {
await server?.close();
});
-test("decodes E31–E39 from separate read-only catalogs", async () => {
+test("decodes E31–E40 from separate read-only catalogs", async () => {
const requests = [];
- const items = [e31(), e32(), e33(), e34(), e35(), e37(), e38(), e39()];
+ const items = [
+ e31(), e32(), e33(), e34(), e35(), e37(), e38(), e39(), e40(),
+ ];
const decoded = await fetchAdvancedLaboratoryResults({
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
@@ -578,6 +681,15 @@ test("decodes E31–E39 from separate read-only catalogs", async () => {
assert.equal(decoded.e39.metrics.dimensions.presence.accuracy, 0.849315);
assert.equal(decoded.e39.metrics.highSeverityFailures, 8);
assert.equal(decoded.e39.qualityGatePassed, false);
+ assert.equal(
+ decoded.e40.developmentCrossValidation.protocols
+ .wholeTrackOrSceneWindow.dimensions.presence.accuracy,
+ 0.920588,
+ );
+ assert.equal(decoded.e40.developmentCrossValidation.validationLabelsUsed, false);
+ assert.equal(decoded.e40.metrics.dimensions.presence.accuracy, 0.90411);
+ assert.equal(decoded.e40.metrics.highSeverityFailures, 0);
+ assert.equal(decoded.e40.qualityGatePassed, true);
assert.deepEqual(requests, [
{ input: "/api/v1/laboratory/e31/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e32/results?limit=1", method: "GET" },
@@ -587,9 +699,36 @@ test("decodes E31–E39 from separate read-only catalogs", async () => {
{ input: "/api/v1/laboratory/e37/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e38/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e39/results?limit=1", method: "GET" },
+ { input: "/api/v1/laboratory/e40/results?limit=1", method: "GET" },
]);
});
+test("keeps valid LAB catalogs available when one transport endpoint fails", async () => {
+ const decoded = await fetchAdvancedLaboratoryResults({
+ fetcher: async (input) => {
+ const path = String(input);
+ if (path.includes("/e31/")) {
+ return new Response("", { status: 503 });
+ }
+ const item = path.includes("/e32/") ? e32()
+ : path.includes("/e33/") ? e33()
+ : path.includes("/e34/") ? e34()
+ : path.includes("/e35/") ? e35()
+ : path.includes("/e37/") ? e37()
+ : path.includes("/e38/") ? e38()
+ : path.includes("/e39/") ? e39() : e40();
+ return new Response(JSON.stringify(catalog(item)), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ },
+ });
+
+ assert.equal(decoded.e31, null);
+ assert.equal(decoded.e32.metrics.qualifiedPointsPublished, 2119302);
+ assert.equal(decoded.e40.qualityGatePassed, true);
+});
+
test("rejects authority escalation in an accepted-looking result", async () => {
const forged = {
...e31(),
@@ -604,7 +743,8 @@ test("rejects authority escalation in an accepted-looking result", async () => {
: String(input).includes("/e34/") ? e34()
: String(input).includes("/e35/") ? e35()
: String(input).includes("/e37/") ? e37()
- : String(input).includes("/e38/") ? e38() : e39(),
+ : String(input).includes("/e38/") ? e38()
+ : String(input).includes("/e39/") ? e39() : e40(),
)), { status: 200 }),
}),
AdvancedLaboratoryContractError,
diff --git a/apps/control-station/test/applicationArchitecture.test.mjs b/apps/control-station/test/applicationArchitecture.test.mjs
index 867ba19..ba24b0c 100644
--- a/apps/control-station/test/applicationArchitecture.test.mjs
+++ b/apps/control-station/test/applicationArchitecture.test.mjs
@@ -110,6 +110,8 @@ test("central composition files cannot silently become monoliths again", async (
["App.tsx", 1_250],
["workspaces/Workspaces.tsx", 1_200],
["workspaces/laboratory/LaboratoryArchiveWorkspace.tsx", 1_000],
+ ["core/laboratory/advancedResults.ts", 1_000],
+ ["core/laboratory/e40ProductGate.ts", 500],
["styles/workspaces.css", 4_350],
["styles/laboratory.css", 900],
["styles/laboratory-reporting.css", 100],
diff --git a/apps/control-station/test/laboratoryProductUi.test.mjs b/apps/control-station/test/laboratoryProductUi.test.mjs
index 5183ac3..ae2c144 100644
--- a/apps/control-station/test/laboratoryProductUi.test.mjs
+++ b/apps/control-station/test/laboratoryProductUi.test.mjs
@@ -38,6 +38,10 @@ const e39ResultUrl = new URL(
"../src/workspaces/laboratory/E39Result.tsx",
import.meta.url,
);
+const e40ResultUrl = new URL(
+ "../src/workspaces/laboratory/E40Result.tsx",
+ import.meta.url,
+);
const e35StylesUrl = new URL(
"../src/styles/e35-degradation-recovery.css",
import.meta.url,
@@ -261,6 +265,28 @@ test("E39 reports refinement and the CV-to-validation gap through the canonical
assert.match(advancedSource, / {
+ const [e40Source, advancedSource] = await Promise.all([
+ readFile(e40ResultUrl, "utf8"),
+ readFile(advancedLaboratoryResultUrl, "utf8"),
+ ]);
+
+ assert.match(e40Source, / {
const workspacesSource = await readFile(workspacesUrl, "utf8");