feat(lab): add lazy E40 evidence review

This commit is contained in:
DCCONSTRUCTIONS
2026-07-30 21:53:04 +03:00
parent e9bbfb9a41
commit fdcaf57ae7
24 changed files with 3570 additions and 165 deletions
@@ -0,0 +1,243 @@
import {
AdvancedLaboratoryContractError,
fetchOne,
parseE31,
parseE32,
parseE33,
parseE37,
parseE38,
parseE39,
type AdvancedLaboratoryResults,
type LaboratoryFetch,
} from "./advancedResults";
import { fetchE34TemporalLayerResult } from "./e34TemporalLayer";
import { fetchE35DegradationRecoveryResult } from "./e35DegradationRecovery";
import { fetchE40ProductGateResult } from "./e40ProductGate";
export type AdvancedLaboratoryWorkId =
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
| "e34-temporal-layer"
| "e35-degradation-recovery"
| "e37-ravnoves-acceptance"
| "e38-perception-baseline"
| "e39-perception-refinement"
| "e40-perception-product-gate";
export interface AdvancedLaboratoryIndexItem {
workId: AdvancedLaboratoryWorkId;
resultId: string;
createdAtUtc: string;
}
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"e31-source-binding",
"e32-track-geometry",
"e33-worker-shadow",
"e34-temporal-layer",
"e35-degradation-recovery",
"e37-ravnoves-acceptance",
"e38-perception-baseline",
"e39-perception-refinement",
"e40-perception-product-gate",
];
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"e31-source-binding": "e31-source-qualification",
"e32-track-geometry": "e32-track-geometry",
"e33-worker-shadow": "e33-worker-shadow",
"e34-temporal-layer": "e34-temporal-occupied",
"e35-degradation-recovery": "e35-degradation-recovery",
"e37-ravnoves-acceptance": "e37-ravnoves-acceptance",
"e38-perception-baseline": "e38-perception-baseline",
"e39-perception-refinement": "e39-perception-refinement",
"e40-perception-product-gate": "e40-perception-product-gate",
};
export function isAdvancedLaboratoryWorkId(
value: string,
): value is AdvancedLaboratoryWorkId {
return WORK_IDS.includes(value as AdvancedLaboratoryWorkId);
}
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
return {
e31: null,
e32: null,
e33: null,
e34: null,
e35: null,
e37: null,
e38: null,
e39: null,
e40: null,
};
}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new AdvancedLaboratoryContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function exactString(value: unknown, expected: string, label: string): string {
if (value !== expected) {
throw new AdvancedLaboratoryContractError(`${label}: нарушен контракт.`);
}
return expected;
}
function parseIndexItem(value: unknown): AdvancedLaboratoryIndexItem {
const item = objectValue(value, "Индекс LAB.item");
if (
typeof item.work_id !== "string"
|| !isAdvancedLaboratoryWorkId(item.work_id)
) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.work_id: неизвестная лабораторная работа.",
);
}
const prefix = RESULT_PREFIX[item.work_id];
if (
typeof item.result_id !== "string"
|| !new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(item.result_id)
) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.result_id: нарушена идентичность результата.",
);
}
if (typeof item.created_at_utc !== "string" || !item.created_at_utc.trim()) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.created_at_utc: ожидалась строка.",
);
}
exactString(item.access, "read-only", "Индекс LAB.item.access");
return {
workId: item.work_id,
resultId: item.result_id,
createdAtUtc: item.created_at_utc,
};
}
export async function fetchAdvancedLaboratoryIndex({
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise<readonly AdvancedLaboratoryIndexItem[]> {
const response = await fetcher("/api/v1/laboratory/advanced-index", {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new AdvancedLaboratoryContractError(
`Индекс LAB недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "Индекс LAB");
exactString(
payload.schema_version,
"missioncore.laboratory-advanced-index/v1",
"Индекс LAB.schema_version",
);
exactString(payload.access, "read-only", "Индекс LAB.access");
if (!Array.isArray(payload.items) || payload.items.length > WORK_IDS.length) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.items: нарушен размер каталога.",
);
}
const items = payload.items.map(parseIndexItem);
if (new Set(items.map((item) => item.workId)).size !== items.length) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.items: лабораторная работа продублирована.",
);
}
return items;
}
export function advancedLaboratoryResultAvailable(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
): boolean {
return workId === "e31-source-binding" ? results.e31 !== null
: workId === "e32-track-geometry" ? results.e32 !== null
: workId === "e33-worker-shadow" ? results.e33 !== null
: workId === "e34-temporal-layer" ? results.e34 !== null
: workId === "e35-degradation-recovery" ? results.e35 !== null
: workId === "e37-ravnoves-acceptance" ? results.e37 !== null
: workId === "e38-perception-baseline" ? results.e38 !== null
: workId === "e39-perception-refinement" ? results.e39 !== null
: results.e40 !== null;
}
export async function fetchAdvancedLaboratoryResult(
workId: AdvancedLaboratoryWorkId,
{
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {},
): Promise<AdvancedLaboratoryResults> {
const results = emptyAdvancedLaboratoryResults();
if (workId === "e31-source-binding") {
results.e31 = await fetchOne(
"/api/v1/laboratory/e31/results?limit=1",
parseE31,
fetcher,
signal,
);
} else if (workId === "e32-track-geometry") {
results.e32 = await fetchOne(
"/api/v1/laboratory/e32/results?limit=1",
parseE32,
fetcher,
signal,
);
} else if (workId === "e33-worker-shadow") {
results.e33 = await fetchOne(
"/api/v1/laboratory/e33/results?limit=1",
parseE33,
fetcher,
signal,
);
} else if (workId === "e34-temporal-layer") {
results.e34 = await fetchE34TemporalLayerResult({ fetcher, signal });
} else if (workId === "e35-degradation-recovery") {
results.e35 = await fetchE35DegradationRecoveryResult({ fetcher, signal });
} else if (workId === "e37-ravnoves-acceptance") {
results.e37 = await fetchOne(
"/api/v1/laboratory/e37/results?limit=1",
parseE37,
fetcher,
signal,
);
} else if (workId === "e38-perception-baseline") {
results.e38 = await fetchOne(
"/api/v1/laboratory/e38/results?limit=1",
parseE38,
fetcher,
signal,
);
} else if (workId === "e39-perception-refinement") {
results.e39 = await fetchOne(
"/api/v1/laboratory/e39/results?limit=1",
parseE39,
fetcher,
signal,
);
} else {
results.e40 = await fetchE40ProductGateResult({ fetcher, signal });
}
if (!advancedLaboratoryResultAvailable(workId, results)) {
throw new AdvancedLaboratoryContractError(
"Выбранная лабораторная работа не прошла серверную проверку.",
);
}
return results;
}
@@ -256,7 +256,7 @@ export class AdvancedLaboratoryContractError extends Error {
}
}
type LaboratoryFetch = (
export type LaboratoryFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
@@ -402,7 +402,7 @@ function parseCatalog<T>(
return value.items.length ? parseItem(value.items[0]) : null;
}
function parseE31(value: unknown): E31LaboratoryResult {
export function parseE31(value: unknown): E31LaboratoryResult {
const item = record(value, "E31");
const metrics = record(item.metrics, "E31.metrics");
diagnosticAuthority(item.authority, "E31.authority");
@@ -437,7 +437,7 @@ function parseE31(value: unknown): E31LaboratoryResult {
};
}
function parseE32(value: unknown): E32LaboratoryResult {
export function parseE32(value: unknown): E32LaboratoryResult {
const item = record(value, "E32");
const metrics = record(item.metrics, "E32.metrics");
diagnosticAuthority(item.authority, "E32.authority");
@@ -470,7 +470,7 @@ function parseE32(value: unknown): E32LaboratoryResult {
};
}
function parseE33(value: unknown): E33LaboratoryResult {
export function parseE33(value: unknown): E33LaboratoryResult {
const item = record(value, "E33");
const worker = record(item.worker, "E33.worker");
const metrics = record(item.metrics, "E33.metrics");
@@ -521,7 +521,7 @@ function parseE33(value: unknown): E33LaboratoryResult {
};
}
function parseE37(value: unknown): E37AcceptanceContractResult {
export function parseE37(value: unknown): E37AcceptanceContractResult {
const item = record(value, "E37");
const metrics = record(item.metrics, "E37.metrics");
const distributions = record(
@@ -718,7 +718,7 @@ function parseE38Dimension(
};
}
function parseE38(value: unknown): E38PerceptionBaselineResult {
export function parseE38(value: unknown): E38PerceptionBaselineResult {
const item = record(value, "E38");
const metrics = record(item.metrics, "E38.metrics");
const dimensions = record(metrics.dimensions, "E38.metrics.dimensions");
@@ -810,7 +810,7 @@ function parseDevelopmentDimension(
};
}
function parseE39(value: unknown): E39PerceptionRefinementResult {
export function parseE39(value: unknown): E39PerceptionRefinementResult {
const item = record(value, "E39");
const metrics = record(item.metrics, "E39.metrics");
const dimensions = record(metrics.dimensions, "E39.metrics.dimensions");
@@ -944,7 +944,7 @@ function parseE39(value: unknown): E39PerceptionRefinementResult {
};
}
async function fetchOne<T>(
export async function fetchOne<T>(
path: string,
parser: (value: unknown) => T,
fetcher: LaboratoryFetch,
@@ -59,6 +59,8 @@ export interface E30ReviewItem {
materialization: {
framePointCount: number;
projectedPointCount: number;
projectionWidth: number;
projectionHeight: number;
candidatePointCount: number;
selectedPointCount: number;
rejectedCandidatePointCount: number;
@@ -428,6 +430,14 @@ function parseItem(value: unknown): E30ReviewItem {
materialized.projected_point_count,
"E30 materialization.projected_point_count",
),
projectionWidth: integerValue(
materialized.projection_width,
"E30 materialization.projection_width",
),
projectionHeight: integerValue(
materialized.projection_height,
"E30 materialization.projection_height",
),
candidatePointCount: integerValue(
materialized.candidate_point_count,
"E30 materialization.candidate_point_count",
@@ -0,0 +1,538 @@
export const E40_CASE_DIMENSIONS = [
"presence",
"geometry_association",
"freshness",
] as const;
export type E40CaseDimension = (typeof E40_CASE_DIMENSIONS)[number];
export type E40CaseSeverity = "high" | "medium" | "standard";
export type E40CaseStratum =
| "agree"
| "camera-only"
| "conflict"
| "geometry-only"
| "unknown";
export type E40PredictionBasis =
| "camera-only-softmax"
| "fixed-stratum-policy";
export interface E40CaseState {
presence: "background-or-noise" | "object-present" | "occupied-environment";
geometryAssociation:
| "independent-occupied"
| "insufficient-support"
| "object-associated"
| "rejected-nonobject"
| "unknown";
freshness: "current" | "stale" | "unavailable";
}
export interface E40ErrorCase {
itemId: string;
sequence: number;
sourceFrameIndex: number;
sourceStratum: E40CaseStratum;
severity: E40CaseSeverity;
presenceConfidence: number;
predictionBasis: E40PredictionBasis;
reference: E40CaseState;
prediction: E40CaseState;
mismatchedDimensions: readonly E40CaseDimension[];
access: "read-only";
}
export interface E40CaseCatalog {
resultId: string;
materializationId: string;
items: readonly E40ErrorCase[];
total: number;
truncated: boolean;
access: "read-only";
}
export type E40OperatorVerdict = "confirmed-error" | "rejected-error";
export interface E40OperatorDecision {
itemId: string;
verdict: E40OperatorVerdict;
revision: number;
idempotencyKey: string;
decidedAtUtc: string;
eventId: string;
}
export interface E40OperatorReview {
reviewId: string;
resultId: string;
materializationId: string;
caseCatalogSha256: string;
itemSetSha256: string;
itemCount: number;
reviewerId: string;
revision: number;
decisions: readonly E40OperatorDecision[];
reviewedItemCount: number;
remainingItemCount: number;
updatedAtUtc: string | null;
access: "review-write";
}
export class E40CaseReviewContractError extends Error {
constructor(message: string) {
super(message);
this.name = "E40CaseReviewContractError";
}
}
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 E40CaseReviewContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new E40CaseReviewContractError(`${label}: ожидалась строка.`);
}
return value;
}
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 E40CaseReviewContractError(`${label}: неверный content id.`);
}
return parsed;
}
function sha256Value(value: unknown, label: string): string {
const parsed = stringValue(value, label);
if (!/^[a-f0-9]{64}$/.test(parsed)) {
throw new E40CaseReviewContractError(`${label}: неверный SHA-256.`);
}
return parsed;
}
function integerValue(value: unknown, label: string): number {
if (
typeof value !== "number"
|| !Number.isSafeInteger(value)
|| value < 0
) {
throw new E40CaseReviewContractError(`${label}: ожидалось целое число.`);
}
return value;
}
function nullableString(value: unknown, label: string): string | null {
if (value === null) return null;
return stringValue(value, label);
}
function probability(value: unknown, label: string): number {
if (
typeof value !== "number"
|| !Number.isFinite(value)
|| value < 0
|| value > 1
) {
throw new E40CaseReviewContractError(`${label}: ожидалась вероятность.`);
}
return value;
}
function exactString<T extends string>(
value: unknown,
expected: T,
label: string,
): T {
if (value !== expected) {
throw new E40CaseReviewContractError(`${label}: неверное значение.`);
}
return expected;
}
function enumValue<T extends string>(
value: unknown,
allowed: readonly T[],
label: string,
): T {
if (typeof value !== "string" || !allowed.includes(value as T)) {
throw new E40CaseReviewContractError(`${label}: значение не поддерживается.`);
}
return value as T;
}
function parseState(value: unknown, label: string): E40CaseState {
const source = record(value, label);
return {
presence: enumValue(
source.presence,
[
"background-or-noise",
"object-present",
"occupied-environment",
],
`${label}.presence`,
),
geometryAssociation: enumValue(
source.geometry_association,
[
"independent-occupied",
"insufficient-support",
"object-associated",
"rejected-nonobject",
"unknown",
],
`${label}.geometry_association`,
),
freshness: enumValue(
source.freshness,
["current", "stale", "unavailable"],
`${label}.freshness`,
),
};
}
function parseCase(value: unknown, index: number): E40ErrorCase {
const label = `E40 case[${index}]`;
const source = record(value, label);
if (!Array.isArray(source.mismatched_dimensions)) {
throw new E40CaseReviewContractError(
`${label}.mismatched_dimensions: ожидался массив.`,
);
}
const mismatchedDimensions = source.mismatched_dimensions.map(
(dimension, dimensionIndex) => enumValue(
dimension,
E40_CASE_DIMENSIONS,
`${label}.mismatched_dimensions[${dimensionIndex}]`,
),
);
if (!mismatchedDimensions.length) {
throw new E40CaseReviewContractError(`${label}: кейс не содержит ошибки.`);
}
return {
itemId: contentId(source.item_id, "e30-review-item", `${label}.item_id`),
sequence: integerValue(source.sequence, `${label}.sequence`),
sourceFrameIndex: integerValue(
source.source_frame_index,
`${label}.source_frame_index`,
),
sourceStratum: enumValue(
source.source_stratum,
["agree", "camera-only", "conflict", "geometry-only", "unknown"],
`${label}.source_stratum`,
),
severity: enumValue(
source.severity,
["high", "medium", "standard"],
`${label}.severity`,
),
presenceConfidence: probability(
source.presence_confidence,
`${label}.presence_confidence`,
),
predictionBasis: enumValue(
source.prediction_basis,
["camera-only-softmax", "fixed-stratum-policy"],
`${label}.prediction_basis`,
),
reference: parseState(source.reference, `${label}.reference`),
prediction: parseState(source.prediction, `${label}.prediction`),
mismatchedDimensions,
access: exactString(source.access, "read-only", `${label}.access`),
};
}
export function parseE40CaseCatalog(payload: unknown): E40CaseCatalog {
const source = record(payload, "E40 case catalog");
exactString(
source.schema_version,
"missioncore.laboratory-e40-case-catalog/v1",
"E40 case catalog.schema_version",
);
if (!Array.isArray(source.items) || source.items.length > 64) {
throw new E40CaseReviewContractError(
"E40 case catalog.items: нарушена граница.",
);
}
return {
resultId: contentId(
source.result_id,
"e40-perception-product-gate",
"E40 case catalog.result_id",
),
materializationId: contentId(
source.materialization_id,
"e30-materialization",
"E40 case catalog.materialization_id",
),
items: source.items.map(parseCase),
total: integerValue(source.total, "E40 case catalog.total"),
truncated: typeof source.truncated === "boolean"
? source.truncated
: (() => {
throw new E40CaseReviewContractError(
"E40 case catalog.truncated: ожидался boolean.",
);
})(),
access: exactString(
source.access,
"read-only",
"E40 case catalog.access",
),
};
}
function parseOperatorDecision(
value: unknown,
index: number,
): E40OperatorDecision {
const label = `E40 operator decision[${index}]`;
const source = record(value, label);
return {
itemId: contentId(source.item_id, "e30-review-item", `${label}.item_id`),
verdict: enumValue(
source.verdict,
["confirmed-error", "rejected-error"],
`${label}.verdict`,
),
revision: integerValue(source.revision, `${label}.revision`),
idempotencyKey: stringValue(
source.idempotency_key,
`${label}.idempotency_key`,
),
decidedAtUtc: stringValue(
source.decided_at_utc,
`${label}.decided_at_utc`,
),
eventId: contentId(
source.event_id,
"e40-operator-verdict",
`${label}.event_id`,
),
};
}
export function parseE40OperatorReview(
payload: unknown,
): E40OperatorReview {
const source = record(payload, "E40 operator review");
exactString(
source.schema_version,
"missioncore.e40-operator-review/v1",
"E40 operator review.schema_version",
);
exactString(
source.protocol,
"sealed-error-adjudication/v1",
"E40 operator review.protocol",
);
const binding = record(source.source, "E40 operator review.source");
if (!Array.isArray(source.decisions) || source.decisions.length > 64) {
throw new E40CaseReviewContractError(
"E40 operator review.decisions: нарушена граница.",
);
}
const decisions = source.decisions.map(parseOperatorDecision);
const reviewedItemCount = integerValue(
source.reviewed_item_count,
"E40 operator review.reviewed_item_count",
);
const remainingItemCount = integerValue(
source.remaining_item_count,
"E40 operator review.remaining_item_count",
);
const itemCount = integerValue(
binding.item_count,
"E40 operator review.source.item_count",
);
if (
reviewedItemCount !== decisions.length
|| reviewedItemCount + remainingItemCount !== itemCount
|| new Set(decisions.map((decision) => decision.itemId)).size
!== decisions.length
) {
throw new E40CaseReviewContractError(
"E40 operator review: счётчики решений несовместимы.",
);
}
return {
reviewId: contentId(
source.review_id,
"e40-operator-review",
"E40 operator review.review_id",
),
resultId: contentId(
binding.result_id,
"e40-perception-product-gate",
"E40 operator review.source.result_id",
),
materializationId: contentId(
binding.materialization_id,
"e30-materialization",
"E40 operator review.source.materialization_id",
),
caseCatalogSha256: sha256Value(
binding.case_catalog_sha256,
"E40 operator review.source.case_catalog_sha256",
),
itemSetSha256: sha256Value(
binding.item_set_sha256,
"E40 operator review.source.item_set_sha256",
),
itemCount,
reviewerId: stringValue(
source.reviewer_id,
"E40 operator review.reviewer_id",
),
revision: integerValue(
source.revision,
"E40 operator review.revision",
),
decisions,
reviewedItemCount,
remainingItemCount,
updatedAtUtc: nullableString(
source.updated_at_utc,
"E40 operator review.updated_at_utc",
),
access: exactString(
source.access,
"review-write",
"E40 operator review.access",
),
};
}
export async function fetchE40CaseCatalog(
resultId: string,
{
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {},
): Promise<E40CaseCatalog> {
const response = await fetcher(
`/api/v1/laboratory/e40/results/${encodeURIComponent(resultId)}/cases?limit=48`,
{
method: "GET",
headers: { Accept: "application/json" },
signal,
},
);
if (!response.ok) {
throw new E40CaseReviewContractError(
`Case-review E40 недоступен: HTTP ${response.status}.`,
);
}
const catalog = parseE40CaseCatalog(await response.json());
if (catalog.resultId !== resultId) {
throw new E40CaseReviewContractError(
"Case-review E40 вернул другой результат.",
);
}
return catalog;
}
export async function fetchE40OperatorReview(
resultId: string,
{
reviewerId = "DC",
fetcher = fetch,
signal,
}: {
reviewerId?: string;
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {},
): Promise<E40OperatorReview> {
const response = await fetcher(
`/api/v1/laboratory/e40/results/${encodeURIComponent(resultId)}`
+ `/operator-review?reviewer_id=${encodeURIComponent(reviewerId)}`,
{
method: "GET",
headers: { Accept: "application/json" },
signal,
},
);
if (!response.ok) {
throw new E40CaseReviewContractError(
`Операторская проверка E40 недоступна: HTTP ${response.status}.`,
);
}
const review = parseE40OperatorReview(await response.json());
if (review.resultId !== resultId || review.reviewerId !== reviewerId) {
throw new E40CaseReviewContractError(
"Операторская проверка E40 вернула другой источник.",
);
}
return review;
}
export async function saveE40OperatorVerdict(
resultId: string,
itemId: string,
{
reviewerId = "DC",
expectedRevision,
idempotencyKey,
verdict,
fetcher = fetch,
signal,
}: {
reviewerId?: string;
expectedRevision: number;
idempotencyKey: string;
verdict: E40OperatorVerdict;
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
},
): Promise<E40OperatorReview> {
const response = await fetcher(
`/api/v1/laboratory/e40/results/${encodeURIComponent(resultId)}`
+ `/operator-review/decisions/${encodeURIComponent(itemId)}`,
{
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
reviewer_id: reviewerId,
expected_revision: expectedRevision,
idempotency_key: idempotencyKey,
verdict,
}),
signal,
},
);
if (!response.ok) {
throw new E40CaseReviewContractError(
`Решение E40 не сохранено: HTTP ${response.status}.`,
);
}
const review = parseE40OperatorReview(await response.json());
if (
review.resultId !== resultId
|| review.reviewerId !== reviewerId
|| !review.decisions.some(
(decision) => decision.itemId === itemId
&& decision.verdict === verdict,
)
) {
throw new E40CaseReviewContractError(
"Решение E40 не подтверждено сервером.",
);
}
return review;
}