feat(lab): complete E30 evidence review gate

This commit is contained in:
DCCONSTRUCTIONS
2026-07-27 11:00:32 +03:00
parent a44d7627fd
commit 001d597a89
55 changed files with 15897 additions and 1548 deletions
@@ -0,0 +1,659 @@
import {
parseE30ReviewItem,
type E30ReviewItem,
type E30Stratum,
} from "./e30Review";
export type E30EngineeringVerdict =
| "confirmed"
| "corrected"
| "insufficient-evidence";
export type E30DetectorAssessment =
| "valid"
| "class-mismatch"
| "false-positive"
| "missed-object"
| "not-applicable"
| "insufficient-evidence";
export type E30ProjectionAssessment =
| "aligned"
| "misaligned"
| "not-assessable";
export type E30PointOwnership =
| "object"
| "surface-or-background"
| "static-environment"
| "self"
| "insufficient-support"
| "not-applicable"
| "mixed"
| "insufficient-evidence";
export interface E30ExceptionReviewPrompt {
question: string;
focus: string;
effects: Readonly<{
"object-present": string;
"background-or-noise": string;
"insufficient-evidence": string;
}>;
}
export interface E30EngineeringGeneration {
generationId: string;
createdAtUtc: string;
materializationId: string;
producer: {
producerId: string;
methodId: string;
reviewSheetIdentitySha256: string;
claimsHumanGroundTruth: false;
};
summary: {
itemCount: number;
reviewedItemCount: number;
verdictDistribution: Readonly<Record<string, number>>;
detectorDistribution: Readonly<Record<string, number>>;
projectionDistribution: Readonly<Record<string, number>>;
pointOwnershipDistribution: Readonly<Record<string, number>>;
humanExceptionCount: number;
meanConfidence: number;
};
causeDistribution: readonly {
reasonCode: string;
count: number;
}[];
humanExceptions: readonly {
itemId: string;
reviewKey: string;
sourceStratum: E30Stratum;
confidence: number;
reviewPrompt: E30ExceptionReviewPrompt | null;
}[];
aiReviewComplete: true;
humanExceptionComplete: boolean;
humanReviewComplete: false;
labPublished: false;
access: "read-only";
}
export interface E30EngineeringCatalog {
configured: boolean;
candidateTotal: number;
invalidTotal: number;
items: readonly E30EngineeringGeneration[];
}
export interface E30EngineeringDecision {
sequence: number;
itemId: string;
reviewKey: string;
sourceStratum: E30Stratum;
verdict: E30EngineeringVerdict;
effectiveStratum: E30Stratum | null;
detectorAssessment: E30DetectorAssessment;
projectionAssessment: E30ProjectionAssessment;
pointOwnership: E30PointOwnership;
causeCode: string | null;
confidence: number;
humanExceptionRequired: boolean;
exceptionReason: "ambiguity" | "high-impact" | null;
evidenceNote: string;
reviewSheet: {
path: string;
sha256: string;
ordinal: number;
};
}
export interface E30EngineeringExceptions {
resultId: string;
generationId: string;
items: readonly E30ReviewItem[];
total: number;
nextCursor: number | null;
}
export class E30EngineeringContractError extends Error {
constructor(message: string) {
super(message);
this.name = "E30EngineeringContractError";
}
}
type E30Fetch = (
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 E30EngineeringContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new E30EngineeringContractError(`${label}: ожидалась строка.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
if (
typeof value !== "number"
|| !Number.isSafeInteger(value)
|| value < 0
) {
throw new E30EngineeringContractError(`${label}: ожидалось целое число.`);
}
return value;
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new E30EngineeringContractError(`${label}: ожидалось число.`);
}
return value;
}
function booleanValue(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new E30EngineeringContractError(`${label}: ожидался boolean.`);
}
return value;
}
function falseValue(value: unknown, label: string): false {
if (value !== false) {
throw new E30EngineeringContractError(`${label}: ожидалось false.`);
}
return false;
}
function trueValue(value: unknown, label: string): true {
if (value !== true) {
throw new E30EngineeringContractError(`${label}: ожидалось true.`);
}
return true;
}
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 E30EngineeringContractError(`${label}: неверный content id.`);
}
return parsed;
}
function readOnly(value: unknown, label: string): "read-only" {
if (value !== "read-only") {
throw new E30EngineeringContractError(`${label}: ожидалось read-only.`);
}
return "read-only";
}
function authority(value: unknown, label: string): void {
const parsed = record(value, label);
falseValue(parsed.commands_enabled, `${label}.commands_enabled`);
falseValue(
parsed.navigation_or_safety_accepted,
`${label}.navigation_or_safety_accepted`,
);
}
function distribution(
value: unknown,
label: string,
): Readonly<Record<string, number>> {
const parsed = record(value, label);
return Object.fromEntries(
Object.entries(parsed).map(([key, count]) => [
key,
integerValue(count, `${label}.${key}`),
]),
);
}
function stratum(value: unknown, label: string): E30Stratum {
if (
value !== "conflict"
&& value !== "agree"
&& value !== "camera-only"
&& value !== "unknown"
&& value !== "geometry-only"
) {
throw new E30EngineeringContractError(`${label}: неизвестная страта.`);
}
return value;
}
function nullableStratum(value: unknown, label: string): E30Stratum | null {
return value === null ? null : stratum(value, label);
}
function enumValue<T extends string>(
value: unknown,
values: readonly T[],
label: string,
): T {
if (typeof value !== "string" || !values.includes(value as T)) {
throw new E30EngineeringContractError(`${label}: неизвестное значение.`);
}
return value as T;
}
function exceptionReviewPrompt(
value: unknown,
label: string,
): E30ExceptionReviewPrompt | null {
if (value === null || value === undefined) return null;
const source = record(value, label);
const effects = record(source.effects, `${label}.effects`);
return {
question: stringValue(source.question, `${label}.question`),
focus: stringValue(source.focus, `${label}.focus`),
effects: {
"object-present": stringValue(
effects["object-present"],
`${label}.effects.object-present`,
),
"background-or-noise": stringValue(
effects["background-or-noise"],
`${label}.effects.background-or-noise`,
),
"insufficient-evidence": stringValue(
effects["insufficient-evidence"],
`${label}.effects.insufficient-evidence`,
),
},
};
}
function parseGeneration(value: unknown): E30EngineeringGeneration {
const source = record(value, "A3 generation");
const producer = record(source.producer, "A3 producer");
const summary = record(source.summary, "A3 summary");
const causes = record(source.cause_distribution, "A3 causes");
const humanExceptions = source.human_exceptions;
if (
producer.kind !== "ai-assisted-engineering-review"
|| !Array.isArray(causes.reasons)
|| !Array.isArray(humanExceptions)
) {
throw new E30EngineeringContractError("A3 provenance не поддерживается.");
}
falseValue(
producer.claims_human_ground_truth,
"A3 producer.claims_human_ground_truth",
);
authority(source.authority, "A3 authority");
return {
generationId: contentId(
source.generation_id,
"e30-engineering-generation",
"A3 generation_id",
),
createdAtUtc: stringValue(source.created_at_utc, "A3 created_at_utc"),
materializationId: contentId(
source.materialization_id,
"e30-materialization",
"A3 materialization_id",
),
producer: {
producerId: stringValue(producer.producer_id, "A3 producer_id"),
methodId: stringValue(producer.method_id, "A3 method_id"),
reviewSheetIdentitySha256: stringValue(
producer.review_sheet_identity_sha256,
"A3 review_sheet_identity_sha256",
),
claimsHumanGroundTruth: false,
},
summary: {
itemCount: integerValue(summary.item_count, "A3 item_count"),
reviewedItemCount: integerValue(
summary.reviewed_item_count,
"A3 reviewed_item_count",
),
verdictDistribution: distribution(
summary.verdict_distribution,
"A3 verdict_distribution",
),
detectorDistribution: distribution(
summary.detector_distribution,
"A3 detector_distribution",
),
projectionDistribution: distribution(
summary.projection_distribution,
"A3 projection_distribution",
),
pointOwnershipDistribution: distribution(
summary.point_ownership_distribution,
"A3 point_ownership_distribution",
),
humanExceptionCount: integerValue(
summary.human_exception_count,
"A3 human_exception_count",
),
meanConfidence: numberValue(
summary.mean_confidence,
"A3 mean_confidence",
),
},
causeDistribution: causes.reasons.map((value, index) => {
const reason = record(value, `A3 causes[${index}]`);
return {
reasonCode: stringValue(
reason.reason_code,
`A3 causes[${index}].reason_code`,
),
count: integerValue(reason.count, `A3 causes[${index}].count`),
};
}),
humanExceptions: humanExceptions.map((value, index) => {
const exception = record(value, `A3 exceptions[${index}]`);
const confidence = numberValue(
exception.confidence,
`A3 exceptions[${index}].confidence`,
);
if (confidence < 0 || confidence > 1) {
throw new E30EngineeringContractError(
`A3 exceptions[${index}].confidence вне диапазона.`,
);
}
return {
itemId: contentId(
exception.item_id,
"e30-review-item",
`A3 exceptions[${index}].item_id`,
),
reviewKey: stringValue(
exception.review_key,
`A3 exceptions[${index}].review_key`,
),
sourceStratum: stratum(
exception.source_stratum,
`A3 exceptions[${index}].source_stratum`,
),
confidence,
reviewPrompt: exceptionReviewPrompt(
exception.review_prompt,
`A3 exceptions[${index}].review_prompt`,
),
};
}),
aiReviewComplete: trueValue(
source.ai_review_complete,
"A3 ai_review_complete",
),
humanExceptionComplete: booleanValue(
source.human_exception_complete,
"A3 human_exception_complete",
),
humanReviewComplete: falseValue(
source.human_review_complete,
"A3 human_review_complete",
),
labPublished: falseValue(source.lab_published, "A3 lab_published"),
access: readOnly(source.access, "A3 access"),
};
}
export function parseE30EngineeringCatalog(
value: unknown,
): E30EngineeringCatalog {
const source = record(value, "A3 catalog");
if (
source.schema_version
!== "missioncore.laboratory-e30-engineering-generations/v1"
|| !Array.isArray(source.items)
) {
throw new E30EngineeringContractError("A3 catalog schema не поддерживается.");
}
readOnly(source.access, "A3 catalog access");
return {
configured: Boolean(source.configured),
candidateTotal: integerValue(
source.candidate_total,
"A3 candidate_total",
),
invalidTotal: integerValue(source.invalid_total, "A3 invalid_total"),
items: source.items.map(parseGeneration),
};
}
export function parseE30EngineeringDecision(
value: unknown,
): E30EngineeringDecision {
const response = record(value, "A3 decision response");
if (
response.schema_version
!== "missioncore.laboratory-e30-engineering-decision/v1"
) {
throw new E30EngineeringContractError("A3 decision schema не поддерживается.");
}
readOnly(response.access, "A3 decision access");
contentId(
response.generation_id,
"e30-engineering-generation",
"A3 decision generation_id",
);
const source = record(response.decision, "A3 decision");
const sheet = record(source.review_sheet, "A3 review_sheet");
const confidence = numberValue(source.confidence, "A3 confidence");
if (confidence < 0 || confidence > 1) {
throw new E30EngineeringContractError("A3 confidence вне диапазона.");
}
const exceptionReason = source.exception_reason === null
? null
: enumValue(
source.exception_reason,
["ambiguity", "high-impact"] as const,
"A3 exception_reason",
);
return {
sequence: integerValue(source.sequence, "A3 sequence"),
itemId: contentId(
source.item_id,
"e30-review-item",
"A3 decision item_id",
),
reviewKey: stringValue(source.review_key, "A3 review_key"),
sourceStratum: stratum(source.source_stratum, "A3 source_stratum"),
verdict: enumValue(
source.verdict,
["confirmed", "corrected", "insufficient-evidence"] as const,
"A3 verdict",
),
effectiveStratum: nullableStratum(
source.effective_stratum,
"A3 effective_stratum",
),
detectorAssessment: enumValue(
source.detector_assessment,
[
"valid",
"class-mismatch",
"false-positive",
"missed-object",
"not-applicable",
"insufficient-evidence",
] as const,
"A3 detector_assessment",
),
projectionAssessment: enumValue(
source.projection_assessment,
["aligned", "misaligned", "not-assessable"] as const,
"A3 projection_assessment",
),
pointOwnership: enumValue(
source.point_ownership,
[
"object",
"surface-or-background",
"static-environment",
"self",
"insufficient-support",
"not-applicable",
"mixed",
"insufficient-evidence",
] as const,
"A3 point_ownership",
),
causeCode: source.cause_code === null
? null
: stringValue(source.cause_code, "A3 cause_code"),
confidence,
humanExceptionRequired: booleanValue(
source.human_exception_required,
"A3 human_exception_required",
),
exceptionReason,
evidenceNote: stringValue(source.evidence_note, "A3 evidence_note"),
reviewSheet: {
path: stringValue(sheet.path, "A3 review_sheet.path"),
sha256: stringValue(sheet.sha256, "A3 review_sheet.sha256"),
ordinal: integerValue(sheet.ordinal, "A3 review_sheet.ordinal"),
},
};
}
export function parseE30EngineeringExceptions(
value: unknown,
): E30EngineeringExceptions {
const source = record(value, "A3 exception queue");
if (
source.schema_version
!== "missioncore.laboratory-e30-engineering-exceptions/v1"
|| !Array.isArray(source.items)
) {
throw new E30EngineeringContractError(
"A3 exception queue schema не поддерживается.",
);
}
readOnly(source.access, "A3 exception queue access");
return {
resultId: contentId(
source.result_id,
"e30-materialization",
"A3 exception queue result_id",
),
generationId: contentId(
source.generation_id,
"e30-engineering-generation",
"A3 exception queue generation_id",
),
items: source.items.map(parseE30ReviewItem),
total: integerValue(source.total, "A3 exception queue total"),
nextCursor: source.next_cursor === null
? null
: integerValue(
source.next_cursor,
"A3 exception queue next_cursor",
),
};
}
async function responseJson(response: Response, fallback: string): Promise<unknown> {
let payload: unknown = null;
try {
payload = await response.json();
} catch {
// Preserve the status-aware fallback below.
}
if (!response.ok) {
const detail = payload && typeof payload === "object" && "detail" in payload
? String((payload as { detail?: unknown }).detail)
: fallback;
throw new E30EngineeringContractError(detail);
}
return payload;
}
function validMaterializationId(value: string): boolean {
return /^e30-materialization-[a-f0-9]{64}$/.test(value);
}
function validGenerationId(value: string): boolean {
return /^e30-engineering-generation-[a-f0-9]{64}$/.test(value);
}
function validItemId(value: string): boolean {
return /^e30-review-item-[a-f0-9]{64}$/.test(value);
}
export async function fetchE30EngineeringCatalog(
resultId: string,
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30EngineeringCatalog> {
if (!validMaterializationId(resultId)) {
throw new E30EngineeringContractError("Некорректный E30 materialization id.");
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/laboratory/e30/reviews/${resultId}/engineering-generations?limit=1`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseE30EngineeringCatalog(
await responseJson(response, "Не удалось получить A3 generation."),
);
}
export async function fetchE30EngineeringDecision(
resultId: string,
generationId: string,
itemId: string,
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30EngineeringDecision> {
if (
!validMaterializationId(resultId)
|| !validGenerationId(generationId)
|| !validItemId(itemId)
) {
throw new E30EngineeringContractError("Некорректный A3 decision id.");
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/laboratory/e30/reviews/${resultId}/engineering-generations/`
+ `${generationId}/items/${itemId}`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseE30EngineeringDecision(
await responseJson(response, "Не удалось получить A3 decision."),
);
}
export async function fetchE30EngineeringExceptions(
resultId: string,
generationId: string,
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30EngineeringExceptions> {
if (!validMaterializationId(resultId) || !validGenerationId(generationId)) {
throw new E30EngineeringContractError("Некорректная A3 exception queue.");
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/laboratory/e30/reviews/${resultId}/engineering-generations/`
+ `${generationId}/exceptions?limit=128`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseE30EngineeringExceptions(
await responseJson(response, "Не удалось получить A3 exception queue."),
);
}
@@ -0,0 +1,459 @@
import {
E30ReviewContractError,
type E30Stratum,
} from "./e30Review";
export const E30_EXCEPTION_DISPOSITIONS = [
"object-present",
"background-or-noise",
"insufficient-evidence",
] as const;
export type E30ExceptionDisposition =
(typeof E30_EXCEPTION_DISPOSITIONS)[number];
export type E30HumanReviewState = "active" | "finalized";
export interface E30HumanReviewDecision {
itemId: string;
sourceStratum: E30Stratum;
disposition: E30ExceptionDisposition;
notes: string | null;
eventId: string;
decidedAtUtc: string;
}
export interface E30HumanReviewDraft {
draftId: string;
materializationId: string;
engineeringGenerationId: string;
reviewerId: string;
createdAtUtc: string;
state: E30HumanReviewState;
revision: number;
itemCount: number;
reviewedItemCount: number;
remainingItemCount: number;
dispositionDistribution: Readonly<Record<string, number>>;
generationId: string | null;
decisions: readonly E30HumanReviewDecision[];
labPublished: false;
access: "review-write" | "read-only";
}
export interface E30HumanReviewGeneration {
generationId: string;
materializationId: string;
engineeringGenerationId: string;
reviewerId: string;
createdAtUtc: string;
itemCount: number;
dispositionDistribution: Readonly<Record<string, number>>;
coverage: {
expectedItemCount: number;
reviewedItemCount: number;
complete: true;
};
humanReviewComplete: true;
labPublished: false;
access: "read-only";
}
export interface E30HumanReviewFinalized {
draft: E30HumanReviewDraft;
generation: E30HumanReviewGeneration;
}
type E30Fetch = (
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 E30ReviewContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new E30ReviewContractError(`${label}: ожидалась строка.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
if (
typeof value !== "number"
|| !Number.isSafeInteger(value)
|| value < 0
) {
throw new E30ReviewContractError(`${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 E30ReviewContractError(`${label}: некорректный content id.`);
}
return parsed;
}
function falseValue(value: unknown, label: string): false {
if (value !== false) {
throw new E30ReviewContractError(`${label}: ожидалось false.`);
}
return false;
}
function trueValue(value: unknown, label: string): true {
if (value !== true) {
throw new E30ReviewContractError(`${label}: ожидалось true.`);
}
return true;
}
function nullableString(value: unknown, label: string): string | null {
return value === null ? null : stringValue(value, label);
}
function distribution(
value: unknown,
label: string,
): Readonly<Record<string, number>> {
const source = record(value, label);
return Object.fromEntries(
Object.entries(source).map(([key, count]) => [
key,
integerValue(count, `${label}.${key}`),
]),
);
}
function disposition(
value: unknown,
label: string,
): E30ExceptionDisposition {
if (
typeof value !== "string"
|| !E30_EXCEPTION_DISPOSITIONS.includes(
value as E30ExceptionDisposition,
)
) {
throw new E30ReviewContractError(`${label}: неизвестное решение.`);
}
return value as E30ExceptionDisposition;
}
function stratum(value: unknown, label: string): E30Stratum {
if (
value !== "conflict"
&& value !== "agree"
&& value !== "camera-only"
&& value !== "unknown"
&& value !== "geometry-only"
) {
throw new E30ReviewContractError(`${label}: неизвестная страта.`);
}
return value;
}
function parseDecision(value: unknown): E30HumanReviewDecision {
const source = record(value, "Решение по исключению");
return {
itemId: contentId(
source.item_id,
"e30-review-item",
"Решение.item_id",
),
sourceStratum: stratum(source.source_stratum, "Решение.source_stratum"),
disposition: disposition(source.disposition, "Решение.disposition"),
notes: nullableString(source.notes, "Решение.notes"),
eventId: contentId(
source.event_id,
"e30-review-event",
"Решение.event_id",
),
decidedAtUtc: stringValue(
source.decided_at_utc,
"Решение.decided_at_utc",
),
};
}
export function parseE30HumanReviewDraft(
value: unknown,
): E30HumanReviewDraft {
const source = record(value, "Проверка исключений");
if (
source.schema_version !== "missioncore.e30-human-review-draft/v2"
|| !Array.isArray(source.decisions)
) {
throw new E30ReviewContractError(
"Схема проверки исключений не поддерживается.",
);
}
const state = stringValue(source.state, "Проверка.state");
const access = stringValue(source.access, "Проверка.access");
if (
(state !== "active" && state !== "finalized")
|| (state === "active" && access !== "review-write")
|| (state === "finalized" && access !== "read-only")
) {
throw new E30ReviewContractError("Состояние проверки некорректно.");
}
const itemCount = integerValue(source.item_count, "Проверка.item_count");
const reviewedItemCount = integerValue(
source.reviewed_item_count,
"Проверка.reviewed_item_count",
);
const remainingItemCount = integerValue(
source.remaining_item_count,
"Проверка.remaining_item_count",
);
const decisions = source.decisions.map(parseDecision);
if (
reviewedItemCount + remainingItemCount !== itemCount
|| decisions.length !== reviewedItemCount
) {
throw new E30ReviewContractError("Покрытие проверки расходится.");
}
return {
draftId: contentId(
source.draft_id,
"e30-human-draft",
"Проверка.draft_id",
),
materializationId: contentId(
source.materialization_id,
"e30-materialization",
"Проверка.materialization_id",
),
engineeringGenerationId: contentId(
source.engineering_generation_id,
"e30-engineering-generation",
"Проверка.engineering_generation_id",
),
reviewerId: stringValue(source.reviewer_id, "Проверка.reviewer_id"),
createdAtUtc: stringValue(source.created_at_utc, "Проверка.created_at_utc"),
state,
revision: integerValue(source.revision, "Проверка.revision"),
itemCount,
reviewedItemCount,
remainingItemCount,
dispositionDistribution: distribution(
source.disposition_distribution,
"Проверка.disposition_distribution",
),
generationId: source.generation_id === null
? null
: contentId(
source.generation_id,
"e30-review-generation",
"Проверка.generation_id",
),
decisions,
labPublished: falseValue(source.lab_published, "Проверка.lab_published"),
access: access as "review-write" | "read-only",
};
}
function parseGeneration(value: unknown): E30HumanReviewGeneration {
const source = record(value, "Зафиксированная проверка");
const coverage = record(source.coverage, "Проверка.coverage");
if (
source.schema_version !== "missioncore.e30-human-review-generation/v2"
|| source.access !== "read-only"
) {
throw new E30ReviewContractError(
"Схема зафиксированной проверки не поддерживается.",
);
}
return {
generationId: contentId(
source.generation_id,
"e30-review-generation",
"Проверка.generation_id",
),
materializationId: contentId(
source.materialization_id,
"e30-materialization",
"Проверка.materialization_id",
),
engineeringGenerationId: contentId(
source.engineering_generation_id,
"e30-engineering-generation",
"Проверка.engineering_generation_id",
),
reviewerId: stringValue(source.reviewer_id, "Проверка.reviewer_id"),
createdAtUtc: stringValue(source.created_at_utc, "Проверка.created_at_utc"),
itemCount: integerValue(source.item_count, "Проверка.item_count"),
dispositionDistribution: distribution(
source.disposition_distribution,
"Проверка.disposition_distribution",
),
coverage: {
expectedItemCount: integerValue(
coverage.expected_item_count,
"Проверка.coverage.expected",
),
reviewedItemCount: integerValue(
coverage.reviewed_item_count,
"Проверка.coverage.reviewed",
),
complete: trueValue(coverage.complete, "Проверка.coverage.complete"),
},
humanReviewComplete: trueValue(
source.human_review_complete,
"Проверка.human_review_complete",
),
labPublished: falseValue(source.lab_published, "Проверка.lab_published"),
access: "read-only",
};
}
export function parseE30HumanReviewFinalized(
value: unknown,
): E30HumanReviewFinalized {
const source = record(value, "Фиксация проверки");
if (
source.schema_version
!== "missioncore.laboratory-e30-human-review-finalized/v2"
) {
throw new E30ReviewContractError("Схема фиксации не поддерживается.");
}
const draft = parseE30HumanReviewDraft(source.draft);
const generation = parseGeneration(source.generation);
if (
draft.state !== "finalized"
|| draft.generationId !== generation.generationId
|| draft.engineeringGenerationId !== generation.engineeringGenerationId
) {
throw new E30ReviewContractError("Связь фиксации расходится.");
}
return { draft, generation };
}
async function responseJson(response: Response, fallback: string): Promise<unknown> {
let payload: unknown = null;
try {
payload = await response.json();
} catch {
// Preserve the status-aware fallback below.
}
if (!response.ok) {
const detail = payload && typeof payload === "object" && "detail" in payload
? String((payload as { detail?: unknown }).detail)
: fallback;
throw new E30ReviewContractError(detail);
}
return payload;
}
function validId(value: string, prefix: string): boolean {
return new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(value);
}
function generationQuery(engineeringGenerationId: string): string {
return `?engineering_generation_id=${encodeURIComponent(
engineeringGenerationId,
)}`;
}
export async function createOrResumeE30HumanReview(
resultId: string,
engineeringGenerationId: string,
reviewerId = "DC",
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30HumanReviewDraft> {
if (
!validId(resultId, "e30-materialization")
|| !validId(engineeringGenerationId, "e30-engineering-generation")
) {
throw new E30ReviewContractError("Некорректная очередь проверки.");
}
const response = await (options.fetcher ?? fetch)(
`/api/v1/laboratory/e30/reviews/${resultId}/human-review`,
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
reviewer_id: reviewerId,
engineering_generation_id: engineeringGenerationId,
}),
signal: options.signal,
},
);
return parseE30HumanReviewDraft(
await responseJson(response, "Не удалось открыть проверку."),
);
}
export async function saveE30HumanReviewDecision(
resultId: string,
engineeringGenerationId: string,
draftId: string,
itemId: string,
request: {
expectedRevision: number;
idempotencyKey: string;
disposition: E30ExceptionDisposition;
notes: string | null;
},
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30HumanReviewDraft> {
const response = await (options.fetcher ?? fetch)(
`/api/v1/laboratory/e30/reviews/${resultId}/human-review/${draftId}`
+ `/decisions/${itemId}${generationQuery(engineeringGenerationId)}`,
{
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
expected_revision: request.expectedRevision,
idempotency_key: request.idempotencyKey,
disposition: request.disposition,
notes: request.notes,
}),
signal: options.signal,
},
);
return parseE30HumanReviewDraft(
await responseJson(response, "Не удалось сохранить решение."),
);
}
export async function finalizeE30HumanReview(
resultId: string,
engineeringGenerationId: string,
draftId: string,
expectedRevision: number,
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30HumanReviewFinalized> {
const response = await (options.fetcher ?? fetch)(
`/api/v1/laboratory/e30/reviews/${resultId}/human-review/${draftId}`
+ `/finalize${generationQuery(engineeringGenerationId)}`,
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
expected_revision: expectedRevision,
confirm_generation: true,
}),
signal: options.signal,
},
);
return parseE30HumanReviewFinalized(
await responseJson(response, "Не удалось зафиксировать проверку."),
);
}
@@ -0,0 +1,759 @@
export const E30_STRATA = [
"conflict",
"agree",
"camera-only",
"unknown",
"geometry-only",
] as const;
export type E30Stratum = (typeof E30_STRATA)[number];
export interface E30ReviewResult {
resultId: string;
createdAtUtc: string | null;
reviewPackId: string;
e29ResultId: string;
sourceSessionId: string;
itemCount: number;
stratumCounts: Record<E30Stratum, number>;
reasonTaxonomy: readonly string[];
projection: {
width: number;
height: number;
sourceId: string;
calibrationSlot: string;
};
cameraEvidenceAvailable: boolean;
humanReviewComplete: false;
labPublished: false;
access: "read-only";
}
export interface E30ReviewCatalog {
configured: boolean;
candidateTotal: number;
invalidTotal: number;
items: readonly E30ReviewResult[];
}
export interface E30ReviewSnapshot {
label: string | null;
geometryStatus: string;
geometryReason: string | null;
rangeM: number | null;
nearestRangeM: number | null;
bboxXyxy: readonly [number, number, number, number] | null;
}
export interface E30ReviewItem {
itemId: string;
sequence: number;
reviewKey: string;
stratum: E30Stratum;
rangeBucket: string;
frameIndex: number;
sourceFrameIndex: number;
sessionSeconds: number;
locatorKind: "semantic-observation" | "geometry-only-cluster";
snapshot: E30ReviewSnapshot;
materialization: {
framePointCount: number;
projectedPointCount: number;
candidatePointCount: number;
selectedPointCount: number;
rejectedCandidatePointCount: number;
selectedAndCandidateLossless: true;
freeSpaceValid: false;
humanReviewComplete: false;
detectorScore: number | null;
};
cameraFrameAvailable: boolean;
engineeringTriage: E30EngineeringTriage;
review: {
state: "unreviewed";
reasonCode: null;
notes: null;
};
access: "read-only";
}
export interface E30EngineeringTriage {
provenance: "deterministic-evidence-readiness/v1";
state: "ready-for-ai-review" | "blocked-camera-frame-unavailable";
attention: "standard" | "elevated";
signals: readonly string[];
semanticVerdict: null;
humanExceptionRequired: null;
}
export interface E30ReviewItems {
resultId: string;
stratum: E30Stratum;
items: readonly E30ReviewItem[];
total: number;
nextCursor: number | null;
reasonTaxonomy: readonly string[];
}
export interface E30ReviewItemDetail extends E30ReviewItem {
cameraFrame: {
available: true;
url: string;
sha256: string;
width: number;
height: number;
sourceFrameIndex: number;
exactSourceFrame: true;
} | null;
selected: {
sourceIndices: readonly number[];
pointsMapXyzM: readonly (readonly [number, number, number])[];
};
candidate: {
sourceIndices: readonly number[];
pointsMapXyzM: readonly (readonly [number, number, number])[];
};
projection: {
sourceIndices: readonly number[];
pointsMapXyzM: readonly (readonly [number, number, number])[];
pixelsXy: readonly (readonly [number, number])[];
depthM: readonly number[];
pointClass: readonly number[];
pointHeightM: readonly number[];
candidateMask: readonly number[];
selectedMask: readonly number[];
};
pose: {
positionMapXyzM: readonly [number, number, number];
orientationMapFromLidarXyzw: readonly [number, number, number, number];
};
}
export class E30ReviewContractError extends Error {
constructor(message: string) {
super(message);
this.name = "E30ReviewContractError";
}
}
type E30Fetch = (
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 E30ReviewContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new E30ReviewContractError(`${label}: ожидалась строка.`);
}
return value;
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new E30ReviewContractError(`${label}: ожидалось конечное число.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new E30ReviewContractError(`${label}: ожидалось целое число.`);
}
return parsed;
}
function booleanValue(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new E30ReviewContractError(`${label}: ожидался boolean.`);
}
return value;
}
function falseValue(value: unknown, label: string): false {
if (value !== false) {
throw new E30ReviewContractError(`${label}: ожидалось false.`);
}
return false;
}
function trueValue(value: unknown, label: string): true {
if (value !== true) {
throw new E30ReviewContractError(`${label}: ожидалось true.`);
}
return true;
}
function nullValue(value: unknown, label: string): null {
if (value !== null) {
throw new E30ReviewContractError(`${label}: ожидалось null.`);
}
return null;
}
function readOnly(value: unknown, label: string): "read-only" {
if (value !== "read-only") {
throw new E30ReviewContractError(`${label}: ожидалось read-only.`);
}
return "read-only";
}
function stratumValue(value: unknown, label: string): E30Stratum {
if (
typeof value !== "string"
|| !E30_STRATA.includes(value as E30Stratum)
) {
throw new E30ReviewContractError(`${label}: неизвестная страта.`);
}
return value as E30Stratum;
}
function nullableNumber(value: unknown, label: string): number | null {
return value === null || value === undefined ? null : numberValue(value, label);
}
function nullableString(value: unknown, label: string): string | null {
return value === null || value === undefined ? null : stringValue(value, label);
}
function numberTuple<const N extends number>(
value: unknown,
length: N,
label: string,
): readonly number[] {
if (!Array.isArray(value) || value.length !== length) {
throw new E30ReviewContractError(`${label}: неверная размерность.`);
}
return value.map((item, index) => numberValue(item, `${label}[${index}]`));
}
function numberArray(value: unknown, label: string): readonly number[] {
if (!Array.isArray(value)) {
throw new E30ReviewContractError(`${label}: ожидался массив.`);
}
return value.map((item, index) => numberValue(item, `${label}[${index}]`));
}
function integerArray(value: unknown, label: string): readonly number[] {
if (!Array.isArray(value)) {
throw new E30ReviewContractError(`${label}: ожидался массив.`);
}
return value.map((item, index) => integerValue(item, `${label}[${index}]`));
}
function points(
value: unknown,
dimensions: 2 | 3,
label: string,
): readonly (readonly number[])[] {
if (!Array.isArray(value)) {
throw new E30ReviewContractError(`${label}: ожидался массив точек.`);
}
return value.map((item, index) => (
numberTuple(item, dimensions, `${label}[${index}]`)
));
}
function strings(value: unknown, label: string): readonly string[] {
if (!Array.isArray(value)) {
throw new E30ReviewContractError(`${label}: ожидался массив строк.`);
}
return value.map((item, index) => stringValue(item, `${label}[${index}]`));
}
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 E30ReviewContractError(`${label}: некорректный content id.`);
}
return parsed;
}
function parseResult(value: unknown): E30ReviewResult {
const source = record(value, "E30 result");
const counts = record(source.stratum_counts, "E30 stratum_counts");
const projection = record(source.projection, "E30 projection");
const authority = record(source.authority, "E30 authority");
falseValue(authority.commands_enabled, "E30 authority.commands_enabled");
falseValue(
authority.navigation_or_safety_accepted,
"E30 authority.navigation_or_safety_accepted",
);
return {
resultId: contentId(
source.result_id,
"e30-materialization",
"E30 result_id",
),
createdAtUtc: source.created_at_utc === null
? null
: stringValue(source.created_at_utc, "E30 created_at_utc"),
reviewPackId: contentId(
source.review_pack_id,
"e30-review-pack",
"E30 review_pack_id",
),
e29ResultId: contentId(
source.e29_result_id,
"e29-camera-geometry",
"E30 e29_result_id",
),
sourceSessionId: stringValue(
source.source_session_id,
"E30 source_session_id",
),
itemCount: integerValue(source.item_count, "E30 item_count"),
stratumCounts: Object.fromEntries(E30_STRATA.map((stratum) => [
stratum,
integerValue(counts[stratum], `E30 stratum_counts.${stratum}`),
])) as Record<E30Stratum, number>,
reasonTaxonomy: strings(source.reason_taxonomy, "E30 reason_taxonomy"),
projection: {
width: integerValue(projection.width, "E30 projection.width"),
height: integerValue(projection.height, "E30 projection.height"),
sourceId: stringValue(projection.source_id, "E30 projection.source_id"),
calibrationSlot: stringValue(
projection.calibration_slot,
"E30 projection.calibration_slot",
),
},
cameraEvidenceAvailable: booleanValue(
source.camera_evidence_available,
"E30 camera_evidence_available",
),
humanReviewComplete: falseValue(
source.human_review_complete,
"E30 human_review_complete",
),
labPublished: falseValue(source.lab_published, "E30 lab_published"),
access: readOnly(source.access, "E30 access"),
};
}
function parseSnapshot(value: unknown): E30ReviewSnapshot {
const source = record(value, "E30 snapshot");
const rawBbox = source.bbox_xyxy;
return {
label: nullableString(source.label ?? source.semantic_class, "E30 snapshot.label"),
geometryStatus: stringValue(
source.geometry_status,
"E30 snapshot.geometry_status",
),
geometryReason: nullableString(
source.geometry_reason,
"E30 snapshot.geometry_reason",
),
rangeM: nullableNumber(source.range_m, "E30 snapshot.range_m"),
nearestRangeM: nullableNumber(
source.nearest_range_m,
"E30 snapshot.nearest_range_m",
),
bboxXyxy: rawBbox === null || rawBbox === undefined
? null
: numberTuple(rawBbox, 4, "E30 snapshot.bbox_xyxy") as
readonly [number, number, number, number],
};
}
function parseItem(value: unknown): E30ReviewItem {
const source = record(value, "E30 item");
const locator = record(source.locator, "E30 item.locator");
const materialized = record(
source.materialization,
"E30 item.materialization",
);
const review = record(source.review, "E30 item.review");
const triage = record(
source.engineering_triage,
"E30 item.engineering_triage",
);
const locatorKind = stringValue(locator.kind, "E30 item.locator.kind");
if (
locatorKind !== "semantic-observation"
&& locatorKind !== "geometry-only-cluster"
) {
throw new E30ReviewContractError("E30 item.locator.kind не поддерживается.");
}
if (review.state !== "unreviewed") {
throw new E30ReviewContractError("E30 item уже изменён вне review gate.");
}
if (
triage.provenance !== "deterministic-evidence-readiness/v1"
|| (
triage.state !== "ready-for-ai-review"
&& triage.state !== "blocked-camera-frame-unavailable"
)
|| (triage.attention !== "standard" && triage.attention !== "elevated")
) {
throw new E30ReviewContractError("E30 engineering triage не поддерживается.");
}
return {
itemId: contentId(
source.item_id,
"e30-review-item",
"E30 item.item_id",
),
sequence: integerValue(source.sequence, "E30 item.sequence"),
reviewKey: stringValue(source.review_key, "E30 item.review_key"),
stratum: stratumValue(source.stratum, "E30 item.stratum"),
rangeBucket: stringValue(source.range_bucket, "E30 item.range_bucket"),
frameIndex: integerValue(source.frame_index, "E30 item.frame_index"),
sourceFrameIndex: integerValue(
source.source_frame_index,
"E30 item.source_frame_index",
),
sessionSeconds: numberValue(
source.session_seconds,
"E30 item.session_seconds",
),
locatorKind,
snapshot: parseSnapshot(source.snapshot),
materialization: {
framePointCount: integerValue(
materialized.frame_point_count,
"E30 materialization.frame_point_count",
),
projectedPointCount: integerValue(
materialized.projected_point_count,
"E30 materialization.projected_point_count",
),
candidatePointCount: integerValue(
materialized.candidate_point_count,
"E30 materialization.candidate_point_count",
),
selectedPointCount: integerValue(
materialized.selected_point_count,
"E30 materialization.selected_point_count",
),
rejectedCandidatePointCount: integerValue(
materialized.rejected_candidate_point_count,
"E30 materialization.rejected_candidate_point_count",
),
selectedAndCandidateLossless: trueValue(
materialized.selected_and_candidate_lossless,
"E30 materialization.selected_and_candidate_lossless",
),
freeSpaceValid: falseValue(
materialized.free_space_valid,
"E30 materialization.free_space_valid",
),
humanReviewComplete: falseValue(
materialized.human_review_complete,
"E30 materialization.human_review_complete",
),
detectorScore: nullableNumber(
materialized.detector_score,
"E30 materialization.detector_score",
),
},
cameraFrameAvailable: booleanValue(
source.camera_frame_available,
"E30 item.camera_frame_available",
),
engineeringTriage: {
provenance: "deterministic-evidence-readiness/v1",
state: triage.state,
attention: triage.attention,
signals: strings(triage.signals, "E30 engineering_triage.signals"),
semanticVerdict: nullValue(
triage.semantic_verdict,
"E30 engineering_triage.semantic_verdict",
),
humanExceptionRequired: nullValue(
triage.human_exception_required,
"E30 engineering_triage.human_exception_required",
),
},
review: {
state: "unreviewed",
reasonCode: nullValue(review.reason_code, "E30 review.reason_code"),
notes: nullValue(review.notes, "E30 review.notes"),
},
access: readOnly(source.access, "E30 item.access"),
};
}
export function parseE30ReviewItem(value: unknown): E30ReviewItem {
return parseItem(value);
}
export function parseE30ReviewCatalog(value: unknown): E30ReviewCatalog {
const source = record(value, "E30 catalog");
if (source.schema_version !== "missioncore.laboratory-e30-catalog/v1") {
throw new E30ReviewContractError("E30 catalog schema не поддерживается.");
}
if (!Array.isArray(source.items)) {
throw new E30ReviewContractError("E30 catalog.items имеет неверный формат.");
}
readOnly(source.access, "E30 catalog.access");
return {
configured: booleanValue(source.configured, "E30 catalog.configured"),
candidateTotal: integerValue(
source.candidate_total,
"E30 catalog.candidate_total",
),
invalidTotal: integerValue(source.invalid_total, "E30 catalog.invalid_total"),
items: source.items.map(parseResult),
};
}
export function parseE30ReviewItems(value: unknown): E30ReviewItems {
const source = record(value, "E30 items");
if (
source.schema_version !== "missioncore.laboratory-e30-items/v1"
|| !Array.isArray(source.items)
) {
throw new E30ReviewContractError("E30 items schema не поддерживается.");
}
readOnly(source.access, "E30 items.access");
return {
resultId: stringValue(source.result_id, "E30 items.result_id"),
stratum: stratumValue(source.stratum, "E30 items.stratum"),
items: source.items.map(parseItem),
total: integerValue(source.total, "E30 items.total"),
nextCursor: source.next_cursor === null
? null
: integerValue(source.next_cursor, "E30 items.next_cursor"),
reasonTaxonomy: strings(source.reason_taxonomy, "E30 items.reason_taxonomy"),
};
}
export function parseE30ReviewItemDetail(value: unknown): E30ReviewItemDetail {
const response = record(value, "E30 item detail response");
if (response.schema_version !== "missioncore.laboratory-e30-item-detail/v1") {
throw new E30ReviewContractError("E30 detail schema не поддерживается.");
}
readOnly(response.access, "E30 detail access");
const source = record(response.item, "E30 item detail");
const item = parseItem(source);
const selected = record(source.selected, "E30 selected");
const candidate = record(source.candidate, "E30 candidate");
const projection = record(source.projection, "E30 projection detail");
const pose = record(source.pose, "E30 pose");
const cameraFrame = source.camera_frame === null
? null
: record(source.camera_frame, "E30 camera_frame");
const projectedLength = integerArray(
projection.source_indices,
"E30 projection.source_indices",
).length;
const projectionCollections = [
projection.points_map_xyz_m,
projection.pixels_xy,
projection.depth_m,
projection.point_class,
projection.point_height_m,
projection.candidate_mask,
projection.selected_mask,
];
if (
projectionCollections.some(
(collection) => !Array.isArray(collection) || collection.length !== projectedLength,
)
) {
throw new E30ReviewContractError("E30 projection arrays имеют разную длину.");
}
const candidateMask = integerArray(
projection.candidate_mask,
"E30 projection.candidate_mask",
);
const selectedMask = integerArray(
projection.selected_mask,
"E30 projection.selected_mask",
);
if (
candidateMask.some((itemValue) => itemValue !== 0 && itemValue !== 1)
|| selectedMask.some((itemValue) => itemValue !== 0 && itemValue !== 1)
) {
throw new E30ReviewContractError("E30 projection mask должна быть бинарной.");
}
const selectedSourceIndices = integerArray(
selected.source_indices,
"E30 selected.source_indices",
);
const selectedPoints = points(
selected.points_map_xyz_m,
3,
"E30 selected.points_map_xyz_m",
) as readonly (readonly [number, number, number])[];
const candidateSourceIndices = integerArray(
candidate.source_indices,
"E30 candidate.source_indices",
);
const candidatePoints = points(
candidate.points_map_xyz_m,
3,
"E30 candidate.points_map_xyz_m",
) as readonly (readonly [number, number, number])[];
if (
selectedSourceIndices.length !== selectedPoints.length
|| candidateSourceIndices.length !== candidatePoints.length
|| selectedSourceIndices.length !== item.materialization.selectedPointCount
|| candidateSourceIndices.length !== item.materialization.candidatePointCount
) {
throw new E30ReviewContractError("E30 selected/candidate arrays расходятся.");
}
return {
...item,
cameraFrame: cameraFrame === null
? null
: {
available: trueValue(
cameraFrame.available,
"E30 camera_frame.available",
),
url: stringValue(cameraFrame.url, "E30 camera_frame.url"),
sha256: stringValue(cameraFrame.sha256, "E30 camera_frame.sha256"),
width: integerValue(cameraFrame.width, "E30 camera_frame.width"),
height: integerValue(cameraFrame.height, "E30 camera_frame.height"),
sourceFrameIndex: integerValue(
cameraFrame.source_frame_index,
"E30 camera_frame.source_frame_index",
),
exactSourceFrame: trueValue(
cameraFrame.exact_source_frame,
"E30 camera_frame.exact_source_frame",
),
},
selected: {
sourceIndices: selectedSourceIndices,
pointsMapXyzM: selectedPoints,
},
candidate: {
sourceIndices: candidateSourceIndices,
pointsMapXyzM: candidatePoints,
},
projection: {
sourceIndices: integerArray(
projection.source_indices,
"E30 projection.source_indices",
),
pointsMapXyzM: points(
projection.points_map_xyz_m,
3,
"E30 projection.points_map_xyz_m",
) as readonly (readonly [number, number, number])[],
pixelsXy: points(
projection.pixels_xy,
2,
"E30 projection.pixels_xy",
) as readonly (readonly [number, number])[],
depthM: numberArray(projection.depth_m, "E30 projection.depth_m"),
pointClass: integerArray(
projection.point_class,
"E30 projection.point_class",
),
pointHeightM: numberArray(
projection.point_height_m,
"E30 projection.point_height_m",
),
candidateMask,
selectedMask,
},
pose: {
positionMapXyzM: numberTuple(
pose.position_map_xyz_m,
3,
"E30 pose.position_map_xyz_m",
) as readonly [number, number, number],
orientationMapFromLidarXyzw: numberTuple(
pose.orientation_map_from_lidar_xyzw,
4,
"E30 pose.orientation_map_from_lidar_xyzw",
) as readonly [number, number, number, number],
},
};
}
async function responseJson(response: Response, fallback: string): Promise<unknown> {
let payload: unknown = null;
try {
payload = await response.json();
} catch {
// Preserve the status-aware fallback below.
}
if (!response.ok) {
const detail = payload && typeof payload === "object" && "detail" in payload
? String((payload as { detail?: unknown }).detail)
: fallback;
throw new E30ReviewContractError(detail);
}
return payload;
}
function validResultId(value: string): boolean {
return /^e30-materialization-[a-f0-9]{64}$/.test(value);
}
function validItemId(value: string): boolean {
return /^e30-review-item-[a-f0-9]{64}$/.test(value);
}
export async function fetchE30ReviewCatalog(
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30ReviewCatalog> {
const fetcher = options.fetcher ?? fetch;
const response = await fetcher("/api/v1/laboratory/e30/reviews?limit=1", {
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
});
return parseE30ReviewCatalog(
await responseJson(response, "Не удалось получить LAB E30."),
);
}
export async function fetchE30ReviewItems(
resultId: string,
stratum: E30Stratum,
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30ReviewItems> {
if (!validResultId(resultId) || !E30_STRATA.includes(stratum)) {
throw new E30ReviewContractError("Некорректный E30 review query.");
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/laboratory/e30/reviews/${resultId}/items?stratum=${stratum}&limit=128&cursor=0`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseE30ReviewItems(
await responseJson(response, "Не удалось получить выборку LAB E30."),
);
}
export async function fetchE30ReviewItemDetail(
resultId: string,
itemId: string,
options: { signal?: AbortSignal; fetcher?: E30Fetch } = {},
): Promise<E30ReviewItemDetail> {
if (!validResultId(resultId) || !validItemId(itemId)) {
throw new E30ReviewContractError("Некорректный E30 review item id.");
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/laboratory/e30/reviews/${resultId}/items/${itemId}`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseE30ReviewItemDetail(
await responseJson(response, "Не удалось получить доказательство LAB E30."),
);
}