feat(lab): publish E40 perception product gate
This commit is contained in:
@@ -263,6 +263,24 @@ export function LaboratoryConclusion({
|
||||
);
|
||||
}
|
||||
|
||||
export function LaboratoryMetricGrid({
|
||||
metrics,
|
||||
}: {
|
||||
metrics: readonly LaboratoryResultMetric[];
|
||||
}) {
|
||||
return (
|
||||
<div className="laboratory-result-metrics">
|
||||
{metrics.map((metric) => (
|
||||
<div key={metric.label}>
|
||||
<span>{metric.label}</span>
|
||||
<strong>{metric.value}</strong>
|
||||
<small>{metric.hint}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LaboratoryResultSummary({
|
||||
title,
|
||||
status,
|
||||
@@ -285,15 +303,7 @@ export function LaboratoryResultSummary({
|
||||
</div>
|
||||
<StatusBadge tone={statusTone}>{status}</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
{metrics.map((metric) => (
|
||||
<div key={metric.label}>
|
||||
<span>{metric.label}</span>
|
||||
<strong>{metric.value}</strong>
|
||||
<small>{metric.hint}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<LaboratoryMetricGrid metrics={metrics} />
|
||||
<LaboratoryConclusion {...conclusion} />
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -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<AdvancedLaboratoryResults> {
|
||||
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";
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export function settledCatalogValue<T>(
|
||||
result: PromiseSettledResult<T>,
|
||||
): 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;
|
||||
}
|
||||
@@ -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<Record<string, {
|
||||
correct: number;
|
||||
incorrect: number;
|
||||
total: number;
|
||||
accuracy: number;
|
||||
}>>;
|
||||
confusion: readonly {
|
||||
reference: string;
|
||||
prediction: string;
|
||||
count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface E40DevelopmentProtocol {
|
||||
items: number;
|
||||
foldSizes: Readonly<Record<string, number>>;
|
||||
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<Response>;
|
||||
|
||||
function record(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new E40ProductGateContractError(`${label}: ожидался объект.`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
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<T extends string>(
|
||||
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<Record<string, number>> {
|
||||
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<E40PerceptionProductGateResult | null> {
|
||||
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());
|
||||
}
|
||||
@@ -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<WorkspaceRendererProps>;
|
||||
@@ -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 <E40Result rigLabel={rigLabel} result={results.e40} />;
|
||||
}
|
||||
if (workId === "e39-perception-refinement" && results.e39) {
|
||||
return <E39Result rigLabel={rigLabel} result={results.e39} />;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB E40 · leakage-resistant product gate"
|
||||
description="После E39 проверена route-coordinate-free модель: смежные кадры, целые track и scene-window изолированы при development-проверке, а sealed validation исполнен один раз на Worker 006."
|
||||
status={gateLabel}
|
||||
statusTone={result.qualityGatePassed ? "success" : "warning"}
|
||||
facts={[
|
||||
{
|
||||
label: "Конфигурация",
|
||||
value: `${rigLabel} · ${result.sourceDisplayName}`,
|
||||
},
|
||||
{
|
||||
label: "Development",
|
||||
value: `${formatNumber(metrics.developmentItems, 0)} · dual 5-fold`,
|
||||
},
|
||||
{
|
||||
label: "Validation",
|
||||
value: `${formatNumber(metrics.validationItems, 0)} · sealed`,
|
||||
},
|
||||
{
|
||||
label: "Исполнение",
|
||||
value: `Worker 006 · ${result.workerNode}`,
|
||||
},
|
||||
]}
|
||||
brief={{
|
||||
question: "Удерживает ли perception выбранного сенсорного рига не менее 90% по presence, geometry association и freshness после удаления route-coordinate leakage и группировки зависимых кадров?",
|
||||
approach: "Для 340 development-кейсов зафиксированы 125 признаков без frame index, session time, review ordinal, track ID и абсолютных map-координат. Консервативная stratum-policy и camera-only softmax прошли два групповых пятифолдовых протокола; validation labels при выборе и обучении не использовались.",
|
||||
principalResult: `Development: contiguous presence ${percent(contiguous.presence.accuracy)}, grouped presence ${percent(grouped.presence.accuracy)}. Sealed validation: presence ${percent(dimensions.presence.accuracy)}, geometry ${percent(dimensions.geometryAssociation.accuracy)}, freshness ${percent(dimensions.freshness.accuracy)}.`,
|
||||
limitation: "Это source-scoped доказательство только для RAVNOVES00 на инженерно проверенной разметке. Оно не доказывает перенос на другой маршрут, растительность, камеру, риг или живой ровер и не выдаёт навигационных либо safety-полномочий.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
pipelineId: result.profileId,
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: "E37 frozen acceptance contract",
|
||||
version: `${formatNumber(metrics.developmentItems, 0)} development + ${formatNumber(metrics.validationItems, 0)} sealed validation`,
|
||||
role: "неизменяемый denominator и reference-метрики трёх измерений",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "Conservative evidence-stratum policy",
|
||||
version: "agree · conflict · geometry-only · unknown",
|
||||
role: "явные безопасные terminal outcomes без свободного пространства",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "model",
|
||||
name: "Camera-only deterministic softmax",
|
||||
version: "125 route-coordinate-free structured-image features",
|
||||
role: "presence для camera-only при dual grouped development CV",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "runtime",
|
||||
name: "Worker 006",
|
||||
version: result.workerNode,
|
||||
role: "одно sealed evaluation в package-bound контейнере без командных полномочий",
|
||||
identitySha256: null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="GROUPED DEVELOPMENT → SEALED VALIDATION"
|
||||
title="Устойчивость без route-coordinate leakage"
|
||||
kind="diagnostic-model"
|
||||
>
|
||||
<LaboratoryMetricGrid
|
||||
metrics={[
|
||||
{
|
||||
label: "Contiguous time CV",
|
||||
value: percent(contiguous.presence.accuracy),
|
||||
hint: `presence · geometry ${percent(contiguous.geometryAssociation.accuracy)}`,
|
||||
},
|
||||
{
|
||||
label: "Whole track / scene CV",
|
||||
value: percent(grouped.presence.accuracy),
|
||||
hint: `presence · geometry ${percent(grouped.geometryAssociation.accuracy)}`,
|
||||
},
|
||||
{
|
||||
label: "Sealed presence",
|
||||
value: percent(dimensions.presence.accuracy),
|
||||
hint: `${formatNumber(dimensions.presence.correct, 0)} / ${formatNumber(dimensions.presence.total, 0)} · цель 90%`,
|
||||
},
|
||||
{
|
||||
label: "Sealed freshness",
|
||||
value: percent(dimensions.freshness.accuracy),
|
||||
hint: `${formatNumber(dimensions.freshness.correct, 0)} / ${formatNumber(dimensions.freshness.total, 0)} · цель 90%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<LaboratoryResultSummary
|
||||
title={result.qualityGatePassed
|
||||
? "Source-scoped perception gate RAVNOVES00 закрыт"
|
||||
: "Route-coordinate признаки исключены, но product gate ещё не закрыт"}
|
||||
status={gateLabel}
|
||||
statusTone={result.qualityGatePassed ? "success" : "warning"}
|
||||
metrics={[
|
||||
{
|
||||
label: "Presence",
|
||||
value: percent(dimensions.presence.accuracy),
|
||||
hint: `${formatNumber(dimensions.presence.incorrect, 0)} ошибок · цель 90%`,
|
||||
},
|
||||
{
|
||||
label: "Geometry association",
|
||||
value: percent(dimensions.geometryAssociation.accuracy),
|
||||
hint: `${formatNumber(dimensions.geometryAssociation.incorrect, 0)} ошибок · цель 90%`,
|
||||
},
|
||||
{
|
||||
label: "Freshness",
|
||||
value: percent(dimensions.freshness.accuracy),
|
||||
hint: `${formatNumber(dimensions.freshness.incorrect, 0)} ошибок · цель 90%`,
|
||||
},
|
||||
{
|
||||
label: "High severity",
|
||||
value: formatNumber(metrics.highSeverityFailures, 0),
|
||||
hint: `учёт ${percent(metrics.accountingFraction)} · false free ${formatNumber(metrics.falseFreeClaims, 0)}`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: result.qualityGatePassed
|
||||
? "На неизменяемом RAVNOVES00 все три task-level dimension достигли 90%, полный denominator учтён, ложное свободное пространство не опубликовано и high-severity ошибок нет."
|
||||
: `Development-профиль устойчив к двум зависимым разбиениям; sealed evaluation завершён с полным учётом ${formatNumber(metrics.validationItems, 0)} кейсов и без ложного свободного пространства.`,
|
||||
notProved: "Не доказаны независимая физическая ground truth, второй маршрут, другой риг, растительная среда, живой rover runtime, навигация, команды или safety.",
|
||||
decision: result.qualityGatePassed
|
||||
? "Зафиксировать RAVNOVES00 perception gate как закрытый source-scoped этап. Следующий шаг — temporal product state и live-rover replay без расширения полномочий."
|
||||
: `Не принимать source-scoped product gate. Разбирать только оставшиеся sealed ошибки и блокеры (${result.blockingChecks.join(", ")}), не подбирая профиль по validation.`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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, /<E39Result/);
|
||||
});
|
||||
|
||||
test("E40 reports dual grouped qualification and the sealed product gate through the canonical LAB anatomy", async () => {
|
||||
const [e40Source, advancedSource] = await Promise.all([
|
||||
readFile(e40ResultUrl, "utf8"),
|
||||
readFile(advancedLaboratoryResultUrl, "utf8"),
|
||||
]);
|
||||
|
||||
assert.match(e40Source, /<LaboratoryWorkTemplate/);
|
||||
assert.match(e40Source, /<LaboratoryEvidence/);
|
||||
assert.match(e40Source, /<LaboratoryResultSummary/);
|
||||
assert.match(e40Source, /frame index, session time, review ordinal, track ID/);
|
||||
assert.match(e40Source, /validation labels при выборе и обучении не использовались/);
|
||||
assert.match(e40Source, /Whole track \/ scene CV/);
|
||||
assert.match(e40Source, /[Ss]ource-scoped perception gate/);
|
||||
assert.match(e40Source, /не подбирая профиль по validation/);
|
||||
assert.doesNotMatch(
|
||||
e40Source,
|
||||
/className="laboratory-(?:summary|result-summary|result-metrics|result-conclusion)"/,
|
||||
);
|
||||
assert.match(advancedSource, /id: "e40-perception-product-gate"/);
|
||||
assert.match(advancedSource, /<E40Result/);
|
||||
});
|
||||
|
||||
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