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,55 @@
import { StatusBadge } from "@nodedc/ui-react";
import type { E30EngineeringGeneration } from "../../core/laboratory/e30Engineering";
export function E30EngineeringGenerationSummary({
generation,
}: {
generation: E30EngineeringGeneration;
}) {
const summary = generation.summary;
const confirmed = summary.verdictDistribution.confirmed ?? 0;
const corrected = summary.verdictDistribution.corrected ?? 0;
return (
<section
className="e30-engineering-generation"
aria-label="Результат A3 engineering generation"
>
<header>
<div>
<span className="section-eyebrow">A3 · IMMUTABLE ENGINEERING GENERATION</span>
</div>
<StatusBadge
tone={summary.humanExceptionCount ? "warning" : "accent"}
>
{summary.reviewedItemCount} / {summary.itemCount}
</StatusBadge>
</header>
<dl>
<div>
<dt>Подтверждено</dt>
<dd>{confirmed.toLocaleString("ru-RU")}</dd>
</div>
<div>
<dt>Исправлено</dt>
<dd>{corrected.toLocaleString("ru-RU")}</dd>
</div>
<div>
<dt>Исключения</dt>
<dd>{summary.humanExceptionCount.toLocaleString("ru-RU")}</dd>
</div>
<div>
<dt>Средняя уверенность</dt>
<dd>
{(summary.meanConfidence * 100).toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})}
%
</dd>
</div>
</dl>
</section>
);
}
@@ -0,0 +1,74 @@
import type { E30ReviewItemDetail } from "../../core/laboratory/e30Review";
function formatNumber(value: number): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: 3 });
}
function evidenceRange(item: E30ReviewItemDetail): string {
const value = item.snapshot.rangeM ?? item.snapshot.nearestRangeM;
return value === null ? "недоступна" : `${formatNumber(value)} м`;
}
export function E30EvidenceTelemetry({
detail,
mode,
}: {
detail: E30ReviewItemDetail;
mode: "camera" | "3d";
}) {
return (
<aside
className="e30-evidence-telemetry"
aria-label="Параметры доказательства"
>
<span>
{mode === "camera"
? "Точный camera frame · LiDAR projection"
: "Map frame · Z вверх"}
</span>
<dl>
<div>
<dt>Кадр / время</dt>
<dd>
{formatNumber(detail.sourceFrameIndex)}
{" · "}
{formatNumber(detail.sessionSeconds)} с
</dd>
</div>
<div>
<dt>LiDAR</dt>
<dd>{formatNumber(detail.materialization.projectedPointCount)}</dd>
</div>
<div>
<dt>Кандидаты</dt>
<dd>{formatNumber(detail.materialization.candidatePointCount)}</dd>
</div>
<div>
<dt>Выбрано / отклонено</dt>
<dd>
{formatNumber(detail.materialization.selectedPointCount)}
{" / "}
{formatNumber(detail.materialization.rejectedCandidatePointCount)}
</dd>
</div>
<div>
<dt>Класс E29</dt>
<dd>{detail.snapshot.geometryStatus}</dd>
</div>
<div>
<dt>Дальность</dt>
<dd>{evidenceRange(detail)}</dd>
</div>
</dl>
<div>
<span>Точные индексы LiDAR</span>
<code>
{detail.selected.sourceIndices.length
? detail.selected.sourceIndices.slice(0, 18).join(", ")
: "нет выбранных точек"}
{detail.selected.sourceIndices.length > 18 ? " …" : ""}
</code>
</div>
</aside>
);
}
@@ -0,0 +1,83 @@
import {
useEffect,
useRef,
type ReactNode,
} from "react";
import { createPortal } from "react-dom";
import {
Icon,
IconButton,
SegmentedControl,
} from "@nodedc/ui-react";
export interface LaboratoryEvidenceViewerMode<T extends string> {
value: T;
label: string;
}
export function LaboratoryEvidenceViewer<T extends string>({
label,
mode,
modes,
expanded,
onModeChange,
onExpandedChange,
actions,
overlay,
children,
}: {
label: string;
mode: T;
modes: readonly LaboratoryEvidenceViewerMode<T>[];
expanded: boolean;
onModeChange: (mode: T) => void;
onExpandedChange: (expanded: boolean) => void;
actions?: ReactNode;
overlay?: ReactNode;
children: ReactNode;
}) {
const expandButtonRef = useRef<HTMLButtonElement | null>(null);
useEffect(() => {
if (!expanded) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Escape") return;
event.preventDefault();
onExpandedChange(false);
window.requestAnimationFrame(() => expandButtonRef.current?.focus());
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [expanded, onExpandedChange]);
const viewer = (
<section
className="laboratory-evidence-viewer"
data-expanded={expanded ? "true" : undefined}
aria-label={label}
>
<div className="laboratory-evidence-viewer__stage">
{children}
</div>
{overlay}
<div className="laboratory-evidence-viewer__controls">
{actions}
<SegmentedControl
value={mode}
items={[...modes]}
label={`${label}: режим представления`}
onChange={onModeChange}
/>
<IconButton
ref={expandButtonRef}
label={expanded ? `Свернуть ${label}` : `Развернуть ${label}`}
onClick={() => onExpandedChange(!expanded)}
>
<Icon name={expanded ? "minimize" : "expand"} size={16} />
</IconButton>
</div>
</section>
);
return expanded ? createPortal(viewer, document.body) : viewer;
}
@@ -0,0 +1,215 @@
import type { ReactNode } from "react";
import {
Select,
StatusBadge,
} from "@nodedc/ui-react";
export interface LaboratoryOption<T extends string> {
id: T;
label: string;
}
export type LaboratoryExecutionClass =
| "deterministic"
| "ai-inference"
| "hybrid";
export type LaboratoryMethodCompleteness = "complete" | "legacy-partial";
export type LaboratoryEvidenceKind = "recorded-replay" | "diagnostic-model";
export interface LaboratoryMethodComponent {
kind: "source" | "tool" | "model" | "algorithm" | "runtime";
name: string;
version: string;
role: string;
identitySha256: string | null;
}
export interface LaboratoryMethod {
completeness: LaboratoryMethodCompleteness;
executionClass: LaboratoryExecutionClass;
pipelineId: string;
components: readonly LaboratoryMethodComponent[];
}
const EXECUTION_LABELS: Record<LaboratoryExecutionClass, string> = {
deterministic: "Детерминированный",
"ai-inference": "AI inference",
hybrid: "Гибридный",
};
const COMPONENT_LABELS: Record<LaboratoryMethodComponent["kind"], string> = {
source: "Источник",
tool: "Инструмент",
model: "Модель",
algorithm: "Алгоритм",
runtime: "Runtime",
};
export function LaboratorySelector<T extends string>({
eyebrow,
title,
description,
label,
value,
options,
disabled = false,
onChange,
}: {
eyebrow: string;
title: string;
description: string;
label: string;
value: T;
options: readonly LaboratoryOption<T>[];
disabled?: boolean;
onChange: (value: T) => void;
}) {
return (
<section className="laboratory-selector">
<div>
<span className="section-eyebrow">{eyebrow}</span>
<h2>{title}</h2>
<p>{description}</p>
</div>
<div className="laboratory-selector__control">
<span>{label}</span>
<Select
label={`Выбрать: ${label}`}
value={value}
options={options.map((option) => ({
value: option.id,
label: option.label,
}))}
variant="split"
menuWidth="anchor"
disabled={disabled}
onChange={(next) => onChange(next)}
/>
</div>
</section>
);
}
export function LaboratorySummary({
title,
description,
status,
statusTone = "neutral",
facts,
method,
}: {
title: string;
description: string;
status: string;
statusTone?: "neutral" | "success" | "warning" | "danger" | "accent";
facts: readonly { label: string; value: string }[];
method?: LaboratoryMethod | null;
}) {
const methodComplete = method?.completeness === "complete";
return (
<section className="laboratory-summary">
<header>
<div>
<span className="section-eyebrow">ЛАБОРАТОРНАЯ РАБОТА</span>
<h2>{title}</h2>
<p>{description}</p>
</div>
<StatusBadge tone={statusTone}>{status}</StatusBadge>
</header>
<dl className="laboratory-summary__facts">
{facts.map((fact) => (
<div key={fact.label}>
<dt>{fact.label}</dt>
<dd>{fact.value}</dd>
</div>
))}
</dl>
{method ? (
<div className="laboratory-summary__method">
<header>
<div>
<span className="section-eyebrow">МЕТОД</span>
<strong>{method.pipelineId}</strong>
</div>
<small>
{EXECUTION_LABELS[method.executionClass]}
{" · "}
{methodComplete ? "полная идентичность" : "legacy · частично"}
</small>
</header>
<dl className="laboratory-summary__components">
{method.components.map((component, index) => (
<div key={`${component.kind}:${component.name}:${index}`}>
<dt>{COMPONENT_LABELS[component.kind]}</dt>
<dd>
<strong>{component.name}</strong>
<small>
{component.role}
{" · "}
{component.version}
{component.identitySha256
? ` · ${component.identitySha256.slice(0, 12)}`
: ""}
</small>
</dd>
</div>
))}
</dl>
</div>
) : null}
</section>
);
}
export function LaboratoryEvidence({
eyebrow,
title,
kind,
resizable = false,
children,
}: {
eyebrow: string;
title: string;
kind: LaboratoryEvidenceKind;
resizable?: boolean;
children: ReactNode;
}) {
return (
<section
className="lab-result-surface"
data-evidence-kind={kind}
data-resizable={resizable ? "true" : undefined}
>
<header>
<span className="section-eyebrow">{eyebrow}</span>
<strong>{title}</strong>
</header>
<div className="laboratory-evidence-frame">
{children}
</div>
</section>
);
}
export function LaboratoryWorkTemplate({
summary,
evidence,
result = null,
details = null,
}: {
summary: ReactNode;
evidence: ReactNode;
result?: ReactNode;
details?: ReactNode;
}) {
return (
<div className="laboratory-work-template">
{summary}
{evidence}
{result}
{details}
</div>
);
}
@@ -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."),
);
}
+2
View File
@@ -1,6 +1,8 @@
@import "./styles/base.css";
@import "./styles/shell.css";
@import "./styles/workspaces.css";
@import "./styles/laboratory.css";
@import "./styles/e30-human-review.css";
@import "./styles/spatial.css";
@import "./styles/device.css";
@import "./styles/responsive.css";
@@ -0,0 +1,150 @@
.e30-human-review {
display: grid;
gap: 0.65rem;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-panel-item-bg);
padding: 0.75rem;
}
.e30-human-review__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.e30-human-review h3,
.e30-human-review p {
margin: 0;
}
.e30-human-review h3 {
margin-top: 0.28rem;
color: var(--nodedc-text-primary);
font-size: 0.78rem;
}
.e30-human-review p {
max-width: 58rem;
margin-top: 0.28rem;
color: var(--nodedc-text-muted);
font-size: 0.56rem;
line-height: 1.5;
}
.e30-human-review__field > span,
.e30-human-review__actions > span {
color: var(--nodedc-text-muted);
font-size: 0.5rem;
}
.e30-human-review__form {
display: grid;
grid-template-columns: minmax(14rem, 0.55fr) minmax(18rem, 1fr);
align-items: end;
gap: 0.45rem;
}
.e30-human-review__field {
display: grid;
gap: 0.35rem;
}
.e30-human-review__field .nodedc-select-anchor,
.e30-human-review__field .nodedc-select {
width: 100%;
}
.e30-human-review__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.45rem;
}
.e30-human-review__impact {
display: grid;
gap: 0.2rem;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-glass-panel-bg-soft);
padding: 0.55rem 0.65rem;
}
.e30-human-review__impact span {
color: var(--nodedc-text-muted);
font-size: 0.5rem;
}
.e30-human-review__impact strong {
color: var(--nodedc-text-secondary);
font-size: 0.62rem;
font-weight: 650;
}
.e30-human-review__actions > span {
margin-right: auto;
}
.e30-human-review__error {
color: rgb(var(--nodedc-danger-rgb));
}
@media (max-width: 1100px) {
.e30-human-review__form {
grid-template-columns: 1fr;
}
}
.e30-engineering-generation {
display: grid;
gap: 0.65rem;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-panel-item-bg);
padding: 0.75rem;
}
.e30-engineering-generation header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.e30-engineering-generation dl,
.e30-engineering-generation dt,
.e30-engineering-generation dd {
margin: 0;
}
.e30-engineering-generation dl {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.35rem;
}
.e30-engineering-generation dl > div {
display: grid;
gap: 0.18rem;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-glass-panel-bg-soft);
padding: 0.5rem 0.55rem;
}
.e30-engineering-generation dt {
color: var(--nodedc-text-muted);
font-size: 0.5rem;
}
.e30-engineering-generation dd {
color: var(--nodedc-text-primary);
font-size: 0.72rem;
font-weight: 700;
}
@media (max-width: 1100px) {
.e30-engineering-generation dl {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@@ -0,0 +1,868 @@
.lab-archive-workspace,
.lab-result-surface,
.laboratory-work-template {
display: grid;
gap: 0.85rem;
}
.recordings-workspace {
width: 100%;
height: 100%;
min-height: 34rem;
}
.lab-result-surface > header {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0 0.2rem;
}
.lab-result-surface > header strong {
color: var(--nodedc-text-secondary);
font-size: 0.7rem;
}
.lab-archive-workspace {
padding-bottom: 1rem;
}
.laboratory-selector {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(18rem, 0.38fr);
align-items: center;
gap: 1.4rem;
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
padding: 1rem;
}
.laboratory-selector h2,
.laboratory-selector p {
margin: 0;
}
.laboratory-selector h2 {
margin-top: 0.25rem;
color: var(--nodedc-text-primary);
font-size: 1rem;
}
.laboratory-selector p {
max-width: 60rem;
margin-top: 0.35rem;
color: var(--nodedc-text-muted);
font-size: 0.64rem;
line-height: 1.5;
}
.laboratory-selector__control {
display: grid;
gap: 0.35rem;
}
.laboratory-selector__control > span {
color: var(--nodedc-text-muted);
font-size: 0.55rem;
font-weight: 650;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.laboratory-selector__control .nodedc-select-anchor,
.laboratory-selector__control .nodedc-select {
width: 100%;
}
.laboratory-work-output {
display: grid;
gap: 0.85rem;
}
.laboratory-summary,
.laboratory-result-summary {
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
padding: 1rem;
}
.laboratory-summary > header,
.laboratory-result-summary > header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1.5rem;
}
.laboratory-summary h2,
.laboratory-summary p,
.laboratory-summary dl,
.laboratory-result-summary h2,
.laboratory-result-summary p {
margin: 0;
}
.laboratory-summary h2,
.laboratory-result-summary h2 {
margin-top: 0.3rem;
color: var(--nodedc-text-primary);
font-size: 1rem;
letter-spacing: -0.025em;
}
.laboratory-summary > header p,
.laboratory-result-summary > p {
max-width: 66rem;
margin-top: 0.38rem;
color: var(--nodedc-text-muted);
font-size: 0.63rem;
line-height: 1.55;
}
.laboratory-summary__facts {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.4rem;
margin-top: 0.9rem;
}
.laboratory-summary__facts > div,
.laboratory-result-metrics > div {
display: grid;
gap: 0.25rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.035);
padding: 0.7rem;
}
.laboratory-summary__facts dt,
.laboratory-summary__facts dd {
margin: 0;
}
.laboratory-summary__facts dt,
.laboratory-result-metrics span,
.laboratory-result-metrics small,
.laboratory-summary__method small,
.laboratory-summary__components dt {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.laboratory-summary__facts dd,
.laboratory-result-metrics strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.7rem;
font-weight: 660;
text-overflow: ellipsis;
white-space: nowrap;
}
.laboratory-summary__method {
display: grid;
gap: 0.45rem;
margin-top: 0.65rem;
}
.laboratory-summary__method > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0 0.1rem;
}
.laboratory-summary__method > header > div {
display: flex;
min-width: 0;
align-items: baseline;
gap: 0.55rem;
}
.laboratory-summary__method > header strong {
overflow: hidden;
color: var(--nodedc-text-secondary);
font-size: 0.66rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.laboratory-summary__components {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: 0.35rem;
}
.laboratory-summary__components > div {
display: grid;
grid-template-columns: 4.5rem minmax(0, 1fr);
align-items: center;
gap: 0.65rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.025);
padding: 0.58rem 0.65rem;
}
.laboratory-summary__components dt {
text-transform: uppercase;
}
.laboratory-summary__components dd {
display: grid;
min-width: 0;
margin: 0;
gap: 0.15rem;
}
.laboratory-summary__components strong,
.laboratory-summary__components small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.laboratory-summary__components strong {
color: var(--nodedc-text-primary);
font-size: 0.63rem;
}
.laboratory-evidence-frame {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
border-radius: 1rem;
}
.lab-result-surface[data-resizable="true"] .laboratory-evidence-frame {
height: clamp(42rem, 68vh, 58rem);
min-height: 42rem;
resize: vertical;
}
.laboratory-evidence-frame > .spatial-workspace {
height: 100%;
min-height: 0;
}
.laboratory-evidence-frame > .lidar-quality-workspace {
min-height: 42rem;
}
.lab-archive-workspace[data-viewer-focused="true"] {
height: 100%;
min-height: 0;
padding: 0;
}
.lab-archive-workspace[data-viewer-focused="true"] > .laboratory-selector,
.lab-archive-workspace[data-viewer-focused="true"]
.laboratory-work-template > :not(.lab-result-surface),
.lab-archive-workspace[data-viewer-focused="true"] .lab-result-surface > header {
display: none;
}
.lab-archive-workspace[data-viewer-focused="true"] > .laboratory-work-output,
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-work-template,
.lab-archive-workspace[data-viewer-focused="true"] .lab-result-surface,
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-evidence-frame {
height: 100%;
min-height: 0;
}
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-evidence-frame {
resize: none;
}
.laboratory-result-pending {
display: grid;
min-height: 10rem;
place-items: center;
align-content: center;
gap: 0.5rem;
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
color: var(--nodedc-text-muted);
text-align: center;
}
.laboratory-result-pending strong {
color: var(--nodedc-text-secondary);
font-size: 0.75rem;
}
.laboratory-result-pending p {
max-width: 34rem;
margin: 0;
font-size: 0.62rem;
}
.laboratory-result-summary {
display: grid;
gap: 0.85rem;
}
.laboratory-result-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.4rem;
}
.laboratory-result-metrics > div {
min-width: 0;
}
.laboratory-result-metrics strong {
font-size: 0.9rem;
}
.laboratory-frame-review {
display: grid;
gap: 0.85rem;
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
padding: 1rem;
}
.laboratory-frame-review > header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.laboratory-frame-review h2 {
margin: 0.3rem 0 0;
color: var(--nodedc-text-primary);
font-size: 1rem;
}
.laboratory-frame-review__picker {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 0.4rem;
}
.laboratory-frame-review__picker button {
display: grid;
gap: 0.2rem;
min-width: 0;
border: 0;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.035);
padding: 0.65rem 0.75rem;
color: var(--nodedc-text-secondary);
text-align: left;
cursor: pointer;
}
.laboratory-frame-review__picker button:hover,
.laboratory-frame-review__picker button.is-active {
background: rgb(255 255 255 / 0.09);
color: var(--nodedc-text-primary);
}
.laboratory-frame-review__picker span {
font-size: 0.65rem;
font-weight: 660;
}
.laboratory-frame-review__picker small,
.laboratory-frame-review__detail small {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.laboratory-frame-review__state {
display: flex;
min-height: 6rem;
align-items: center;
justify-content: center;
gap: 0.6rem;
color: var(--nodedc-text-muted);
font-size: 0.65rem;
}
.laboratory-frame-review__detail {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.4rem;
}
.laboratory-frame-review__detail > div {
display: grid;
align-content: start;
gap: 0.3rem;
min-width: 0;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.035);
padding: 0.7rem;
}
.laboratory-frame-review__detail > div > span {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.laboratory-frame-review__detail > div > strong {
color: var(--nodedc-text-primary);
font-size: 0.9rem;
}
.laboratory-frame-review__detail .laboratory-frame-review__conflicts {
grid-column: 1 / -1;
}
.laboratory-frame-review__conflicts p {
display: flex;
justify-content: space-between;
gap: 1rem;
margin: 0;
padding-top: 0.45rem;
}
.laboratory-evidence-viewer {
position: relative;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-canvas);
}
.laboratory-evidence-viewer__stage {
position: absolute;
inset: 0;
min-width: 0;
min-height: 0;
}
.laboratory-evidence-viewer__stage > * {
width: 100%;
height: 100%;
}
.laboratory-evidence-viewer__controls {
position: absolute;
top: 0.6rem;
right: 0.6rem;
z-index: 4;
display: flex;
align-items: center;
gap: 0.45rem;
}
.laboratory-evidence-viewer[data-expanded="true"] {
position: fixed;
z-index: var(--nodedc-layer-overlay);
inset: 0;
border-radius: 0;
}
.laboratory-evidence-viewer[data-expanded="true"] .e30-evidence-scene,
.laboratory-evidence-viewer[data-expanded="true"] .e30-projection-scene {
border-radius: 0;
}
.e30-review-workspace {
display: grid;
gap: 0.8rem;
min-width: 0;
min-height: 0;
}
.e30-review-workspace__header,
.e30-review-workspace__case {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.e30-review-workspace h2,
.e30-review-workspace h3,
.e30-review-workspace p {
margin: 0;
}
.e30-review-workspace h2,
.e30-review-workspace h3 {
margin-top: 0.3rem;
color: var(--nodedc-text-primary);
font-size: 1rem;
}
.e30-review-workspace p {
max-width: 62rem;
margin-top: 0.35rem;
color: var(--nodedc-text-muted);
font-size: 0.62rem;
line-height: 1.5;
}
.e30-review-workspace__strata {
display: flex;
width: 100%;
overflow-x: auto;
}
.e30-review-workspace__strata .nodedc-segmented__item {
min-width: max-content;
flex: 1 0 auto;
}
.e30-review-workspace__body {
display: grid;
grid-template-columns: clamp(10.5rem, 13vw, 12rem) minmax(0, 1fr);
gap: 0.6rem;
height: clamp(40rem, 68vh, 54rem);
min-height: 0;
}
.e30-review-workspace__items,
.e30-review-workspace__detail {
min-width: 0;
min-height: 0;
overflow: hidden;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-glass-panel-bg-soft);
}
.e30-review-workspace__items {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 0.3rem;
padding: 0.55rem;
}
.e30-review-workspace__items > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.25rem 0.2rem 0.5rem;
color: var(--nodedc-text-secondary);
font-size: 0.62rem;
}
.e30-review-workspace__items > header small,
.e30-review-workspace__item small {
color: var(--nodedc-text-muted);
font-size: 0.53rem;
}
.e30-review-workspace__item-list {
display: grid;
min-height: 0;
align-content: start;
gap: 0.3rem;
overflow-y: auto;
padding-right: 0.08rem;
}
.e30-review-workspace__item.nodedc-button {
display: grid;
min-height: auto;
justify-content: flex-start;
gap: 0.18rem;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-panel-item-bg);
padding: 0.62rem 0.7rem;
box-shadow: none;
color: var(--nodedc-text-secondary);
text-align: left;
}
.e30-review-workspace__item.nodedc-button:hover {
background: var(--nodedc-panel-item-hover-bg);
}
.e30-review-workspace__item.nodedc-button[data-active="true"] {
background: var(--nodedc-panel-item-active-bg);
color: var(--nodedc-text-primary);
}
.e30-review-workspace__item span,
.e30-review-workspace__item strong,
.e30-review-workspace__item small {
width: 100%;
overflow: hidden;
font-size: 0.61rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.e30-review-workspace__item strong {
color: var(--nodedc-text-primary);
}
.e30-review-workspace__detail {
display: flex;
flex-direction: column;
gap: 0.65rem;
padding: 0.75rem;
}
.e30-review-workspace__case {
min-height: 3.5rem;
}
.e30-review-workspace__case h3 {
font-size: 0.82rem;
}
.e30-review-workspace__state {
display: flex;
flex: 1;
min-height: 10rem;
align-items: center;
justify-content: center;
gap: 0.55rem;
color: var(--nodedc-text-muted);
font-size: 0.62rem;
}
.e30-review-evidence {
display: grid;
min-height: 0;
flex: 1 1 auto;
grid-template-rows: minmax(0, 1fr);
}
.e30-evidence-scene {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-canvas);
}
.e30-projection-scene {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
overflow: hidden;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-canvas);
}
.e30-projection-scene canvas {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.e30-projection-scene__state {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--nodedc-text-muted);
font-size: 0.62rem;
pointer-events: none;
}
.e30-evidence-scene__viewport {
position: absolute;
inset: 0;
}
.e30-evidence-scene__viewport canvas {
display: block;
width: 100%;
height: 100%;
cursor: grab;
touch-action: none;
}
.e30-evidence-scene__viewport canvas:active {
cursor: grabbing;
}
.e30-evidence-scene__toolbar {
position: absolute;
top: 0.6rem;
left: 0.6rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.e30-evidence-scene__toolbar .nodedc-button {
background: var(--nodedc-floating-surface);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e30-evidence-scene__gestures,
.e30-evidence-scene__legend {
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-floating-surface);
color: var(--nodedc-text-secondary);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e30-evidence-scene__gestures {
display: flex;
gap: 0.55rem;
padding: 0.43rem 0.55rem;
}
.e30-evidence-scene__gestures span,
.e30-evidence-scene__legend span {
font-size: 0.5rem;
}
.e30-evidence-scene__legend {
position: absolute;
right: 0.6rem;
bottom: 0.6rem;
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
padding: 0.42rem 0.55rem;
}
.e30-evidence-scene__legend span {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.e30-evidence-scene__legend span::before {
width: 0.38rem;
height: 0.38rem;
border-radius: 50%;
background: var(--nodedc-text-muted);
content: "";
}
.e30-evidence-scene__legend span[data-point="rejected"]::before {
background: rgb(var(--nodedc-warning-rgb));
}
.e30-evidence-scene__legend span[data-point="selected"]::before {
background: rgb(var(--nodedc-accent-rgb));
}
.e30-evidence-scene__legend span[data-point="depth"]::before {
background: linear-gradient(90deg, #4f72ee, #b95ee8, #ee654f);
}
.e30-evidence-telemetry {
position: absolute;
z-index: 3;
left: 0.6rem;
bottom: 0.6rem;
display: grid;
width: min(11.5rem, calc(100% - 1.2rem));
gap: 0.4rem;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-floating-surface);
padding: 0.5rem 0.6rem;
color: var(--nodedc-text-secondary);
backdrop-filter: blur(var(--nodedc-blur-control));
pointer-events: none;
}
.e30-evidence-telemetry > span,
.e30-evidence-telemetry dt,
.e30-evidence-telemetry > div > span {
color: var(--nodedc-text-muted);
font-size: 0.48rem;
}
.e30-evidence-telemetry dl {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.35rem;
margin: 0;
}
.e30-evidence-telemetry dl > div,
.e30-evidence-telemetry > div {
display: grid;
min-width: 0;
gap: 0.12rem;
}
.e30-evidence-telemetry dt,
.e30-evidence-telemetry dd {
margin: 0;
}
.e30-evidence-telemetry dd {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.56rem;
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
.e30-evidence-telemetry code {
overflow: hidden;
color: var(--nodedc-text-secondary);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.48rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.e30-evidence-scene__error {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--nodedc-text-muted);
font-size: 0.62rem;
}
@media (max-width: 1100px) {
.laboratory-selector {
grid-template-columns: minmax(0, 1fr);
}
.laboratory-summary__facts,
.laboratory-result-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.laboratory-frame-review__detail {
grid-template-columns: 1fr;
}
.laboratory-frame-review__detail .laboratory-frame-review__conflicts {
grid-column: auto;
}
.e30-review-workspace__strata {
justify-content: flex-start;
}
.e30-review-workspace__body {
grid-template-columns: 1fr;
height: auto;
}
.e30-review-workspace__items {
height: 18rem;
}
.e30-review-workspace__detail {
min-height: 40rem;
}
.e30-review-evidence {
min-height: 30rem;
}
}
+1 -442
View File
@@ -2384,455 +2384,14 @@
}
.recordings-workspace,
.lab-archive-workspace,
.lab-result-surface,
.laboratory-work-template {
display: grid;
gap: 0.85rem;
}
.recordings-workspace {
display: grid;
width: 100%;
height: 100%;
min-height: 34rem;
}
.lab-result-surface > header {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0 0.2rem;
}
.lab-result-surface > header strong {
color: var(--nodedc-text-secondary);
font-size: 0.7rem;
}
.lab-archive-workspace {
padding-bottom: 1rem;
}
.laboratory-selector {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(18rem, 0.38fr);
align-items: center;
gap: 1.4rem;
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
padding: 1rem;
}
.laboratory-selector h2,
.laboratory-selector p {
margin: 0;
}
.laboratory-selector h2 {
margin-top: 0.25rem;
color: var(--nodedc-text-primary);
font-size: 1rem;
}
.laboratory-selector p {
max-width: 60rem;
margin-top: 0.35rem;
color: var(--nodedc-text-muted);
font-size: 0.64rem;
line-height: 1.5;
}
.laboratory-selector__control {
display: grid;
gap: 0.35rem;
}
.laboratory-selector__control > span {
color: var(--nodedc-text-muted);
font-size: 0.55rem;
font-weight: 650;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.laboratory-selector__control .nodedc-select-anchor,
.laboratory-selector__control .nodedc-select {
width: 100%;
}
.laboratory-work-output {
display: grid;
gap: 0.85rem;
}
.laboratory-task,
.laboratory-method,
.laboratory-result-summary {
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
padding: 1rem;
}
.laboratory-task > header,
.laboratory-method > header,
.laboratory-result-summary > header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1.5rem;
}
.laboratory-task h2,
.laboratory-task p,
.laboratory-task dl,
.laboratory-method h2,
.laboratory-method p,
.laboratory-method ul,
.laboratory-result-summary h2,
.laboratory-result-summary p {
margin: 0;
}
.laboratory-task h2,
.laboratory-method h2,
.laboratory-result-summary h2 {
margin-top: 0.3rem;
color: var(--nodedc-text-primary);
font-size: 1rem;
letter-spacing: -0.025em;
}
.laboratory-task p,
.laboratory-method p,
.laboratory-result-summary > p {
max-width: 66rem;
margin-top: 0.38rem;
color: var(--nodedc-text-muted);
font-size: 0.63rem;
line-height: 1.55;
}
.laboratory-method__summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.4rem;
margin-top: 0.9rem;
}
.laboratory-method__summary > div {
display: grid;
gap: 0.25rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.035);
padding: 0.7rem;
}
.laboratory-method__summary span,
.laboratory-method li > span,
.laboratory-method small {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.laboratory-method__summary strong {
color: var(--nodedc-text-primary);
font-size: 0.7rem;
}
.laboratory-method ul {
display: grid;
gap: 0.35rem;
margin-top: 0.55rem;
padding: 0;
list-style: none;
}
.laboratory-method li {
display: grid;
grid-template-columns: 5.5rem minmax(0, 1fr) auto;
align-items: center;
gap: 0.75rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.025);
padding: 0.62rem 0.7rem;
}
.laboratory-method li > span {
text-transform: uppercase;
}
.laboratory-method li > div {
display: grid;
min-width: 0;
gap: 0.15rem;
}
.laboratory-method li strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.66rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.laboratory-method code {
color: var(--nodedc-text-secondary);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.54rem;
}
.laboratory-task dl {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.4rem;
margin-top: 0.9rem;
}
.laboratory-task dl > div,
.laboratory-result-metrics > div {
display: grid;
gap: 0.25rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.035);
padding: 0.7rem;
}
.laboratory-task dt,
.laboratory-task dd {
margin: 0;
}
.laboratory-task dt,
.laboratory-result-metrics span,
.laboratory-result-metrics small {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.laboratory-task dd,
.laboratory-result-metrics strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.7rem;
font-weight: 660;
text-overflow: ellipsis;
white-space: nowrap;
}
.laboratory-evidence-frame {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
border-radius: 1rem;
}
.lab-result-surface[data-resizable="true"] .laboratory-evidence-frame {
height: clamp(42rem, 68vh, 58rem);
min-height: 42rem;
resize: vertical;
}
.laboratory-evidence-frame > .spatial-workspace {
height: 100%;
min-height: 0;
}
.laboratory-evidence-frame > .lidar-quality-workspace {
min-height: 42rem;
}
.lab-archive-workspace[data-viewer-focused="true"] {
height: 100%;
min-height: 0;
padding: 0;
}
.lab-archive-workspace[data-viewer-focused="true"] > .laboratory-selector,
.lab-archive-workspace[data-viewer-focused="true"]
.laboratory-work-template > :not(.lab-result-surface),
.lab-archive-workspace[data-viewer-focused="true"] .lab-result-surface > header {
display: none;
}
.lab-archive-workspace[data-viewer-focused="true"] > .laboratory-work-output,
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-work-template,
.lab-archive-workspace[data-viewer-focused="true"] .lab-result-surface,
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-evidence-frame {
height: 100%;
min-height: 0;
}
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-evidence-frame {
resize: none;
}
.laboratory-result-pending {
display: grid;
min-height: 10rem;
place-items: center;
align-content: center;
gap: 0.5rem;
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
color: var(--nodedc-text-muted);
text-align: center;
}
.laboratory-result-pending strong {
color: var(--nodedc-text-secondary);
font-size: 0.75rem;
}
.laboratory-result-pending p {
max-width: 34rem;
margin: 0;
font-size: 0.62rem;
}
.laboratory-result-summary {
display: grid;
gap: 0.85rem;
}
.laboratory-result-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.4rem;
}
.laboratory-result-metrics > div {
min-width: 0;
}
.laboratory-result-metrics strong {
font-size: 0.9rem;
}
.laboratory-frame-review {
display: grid;
gap: 0.85rem;
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
padding: 1rem;
}
.laboratory-frame-review > header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.laboratory-frame-review h2 {
margin: 0.3rem 0 0;
color: var(--nodedc-text-primary);
font-size: 1rem;
}
.laboratory-frame-review__picker {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 0.4rem;
}
.laboratory-frame-review__picker button {
display: grid;
gap: 0.2rem;
min-width: 0;
border: 0;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.035);
padding: 0.65rem 0.75rem;
color: var(--nodedc-text-secondary);
text-align: left;
cursor: pointer;
}
.laboratory-frame-review__picker button:hover,
.laboratory-frame-review__picker button.is-active {
background: rgb(255 255 255 / 0.09);
color: var(--nodedc-text-primary);
}
.laboratory-frame-review__picker span {
font-size: 0.65rem;
font-weight: 660;
}
.laboratory-frame-review__picker small,
.laboratory-frame-review__detail small {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.laboratory-frame-review__state {
display: flex;
min-height: 6rem;
align-items: center;
justify-content: center;
gap: 0.6rem;
color: var(--nodedc-text-muted);
font-size: 0.65rem;
}
.laboratory-frame-review__detail {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.4rem;
}
.laboratory-frame-review__detail > div {
display: grid;
align-content: start;
gap: 0.3rem;
min-width: 0;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.035);
padding: 0.7rem;
}
.laboratory-frame-review__detail > div > span {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.laboratory-frame-review__detail > div > strong {
color: var(--nodedc-text-primary);
font-size: 0.9rem;
}
.laboratory-frame-review__detail .laboratory-frame-review__conflicts {
grid-column: 1 / -1;
}
.laboratory-frame-review__conflicts p {
display: flex;
justify-content: space-between;
gap: 1rem;
margin: 0;
padding-top: 0.45rem;
}
@media (max-width: 1100px) {
.laboratory-selector {
grid-template-columns: minmax(0, 1fr);
}
.laboratory-task dl,
.laboratory-result-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.laboratory-frame-review__detail {
grid-template-columns: 1fr;
}
.laboratory-frame-review__detail .laboratory-frame-review__conflicts {
grid-column: auto;
}
}
.dataset-entry {
display: grid;
grid-template-columns: minmax(20rem, 1.2fr) minmax(22rem, 0.8fr);
@@ -0,0 +1,468 @@
import { useEffect, useRef, useState } from "react";
import { Button, Icon } from "@nodedc/ui-react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import type { E30ReviewItemDetail } from "../core/laboratory/e30Review";
interface E30EvidencePointCloudProps {
detail: E30ReviewItemDetail;
}
function tokenColor(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
): THREE.Color {
const value = getComputedStyle(host).getPropertyValue(token).trim();
if (value.startsWith("#")) {
return new THREE.Color(value);
}
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3
? channels
: fallback;
return new THREE.Color(red / 255, green / 255, blue / 255);
}
function createPointTexture(): THREE.CanvasTexture {
const canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 64;
const context = canvas.getContext("2d");
if (context) {
const gradient = context.createRadialGradient(32, 32, 2, 32, 32, 30);
gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
gradient.addColorStop(0.72, "rgba(255, 255, 255, 0.94)");
gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
context.fillStyle = gradient;
context.fillRect(0, 0, 64, 64);
}
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
}
function toMapScenePositions(
pointsMapXyzM: readonly (readonly [number, number, number])[],
positionMapXyzM: readonly [number, number, number],
): Float32Array {
const positions = new Float32Array(pointsMapXyzM.length * 3);
pointsMapXyzM.forEach(([mapX, mapY, mapZ], index) => {
const offset = index * 3;
positions[offset] = mapX - positionMapXyzM[0];
positions[offset + 1] = mapZ - positionMapXyzM[2];
positions[offset + 2] = -(mapY - positionMapXyzM[1]);
});
return positions;
}
function boundsFromPositions(positions: Float32Array): THREE.Box3 {
const bounds = new THREE.Box3();
const point = new THREE.Vector3();
for (let offset = 0; offset < positions.length; offset += 3) {
point.set(positions[offset], positions[offset + 1], positions[offset + 2]);
bounds.expandByPoint(point);
}
return bounds;
}
function evidenceViewFromPositions(positions: Float32Array): {
target: THREE.Vector3;
radius: number;
} {
if (!positions.length) {
return { target: new THREE.Vector3(), radius: 0.65 };
}
const xValues: number[] = [];
const yValues: number[] = [];
const zValues: number[] = [];
for (let offset = 0; offset < positions.length; offset += 3) {
xValues.push(positions[offset]);
yValues.push(positions[offset + 1]);
zValues.push(positions[offset + 2]);
}
const target = new THREE.Vector3(
percentile(xValues, 0.5),
percentile(yValues, 0.5),
percentile(zValues, 0.5),
);
const radii = xValues.map((x, index) => Math.hypot(
x - target.x,
yValues[index] - target.y,
zValues[index] - target.z,
));
return {
target,
radius: Math.max(percentile(radii, 0.9), 0.65),
};
}
function percentile(values: readonly number[], fraction: number): number {
if (!values.length) return 0;
const sorted = [...values].sort((left, right) => left - right);
const index = Math.min(
sorted.length - 1,
Math.max(0, Math.floor((sorted.length - 1) * fraction)),
);
return sorted[index];
}
export function E30EvidencePointCloud({ detail }: E30EvidencePointCloudProps) {
const hostRef = useRef<HTMLDivElement | null>(null);
const contextGeometryRef = useRef<THREE.BufferGeometry | null>(null);
const rejectedGeometryRef = useRef<THREE.BufferGeometry | null>(null);
const selectedGeometryRef = useRef<THREE.BufferGeometry | null>(null);
const contextMaterialRef = useRef<THREE.PointsMaterial | null>(null);
const rejectedMaterialRef = useRef<THREE.PointsMaterial | null>(null);
const selectedMaterialRef = useRef<THREE.PointsMaterial | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null);
const gridRef = useRef<THREE.GridHelper | null>(null);
const viewTargetRef = useRef(new THREE.Vector3());
const viewDistanceRef = useRef(4);
const [renderError, setRenderError] = useState<string | null>(null);
useEffect(() => {
const host = hostRef.current;
if (!host) return;
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: "high-performance",
});
} catch {
setRenderError("Браузер не смог создать WebGL-сцену доказательства E30.");
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(
tokenColor(host, "--nodedc-canvas", [5, 5, 6]),
1,
);
renderer.domElement.setAttribute(
"aria-label",
"Интерактивное 3D-доказательство E30",
);
renderer.domElement.setAttribute("role", "img");
host.prepend(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 500);
camera.position.set(4, 2.5, 4);
cameraRef.current = camera;
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.minDistance = 0.2;
controls.maxDistance = 300;
controls.minPolarAngle = 0.04;
controls.maxPolarAngle = Math.PI - 0.04;
controls.target.set(0, 0, 0);
controls.update();
controlsRef.current = controls;
const pointTexture = createPointTexture();
const contextGeometry = new THREE.BufferGeometry();
const contextMaterial = new THREE.PointsMaterial({
color: tokenColor(host, "--nodedc-text-muted", [147, 151, 159]),
map: pointTexture,
alphaTest: 0.04,
size: 2.2,
sizeAttenuation: false,
transparent: true,
opacity: 0.34,
depthWrite: false,
});
const contextPoints = new THREE.Points(contextGeometry, contextMaterial);
contextPoints.renderOrder = 0;
scene.add(contextPoints);
contextGeometryRef.current = contextGeometry;
contextMaterialRef.current = contextMaterial;
const rejectedGeometry = new THREE.BufferGeometry();
const rejectedMaterial = new THREE.PointsMaterial({
color: tokenColor(host, "--nodedc-warning-rgb", [255, 209, 102]),
map: pointTexture,
alphaTest: 0.04,
size: 5.5,
sizeAttenuation: false,
transparent: true,
opacity: 0.98,
depthWrite: true,
});
const rejectedPoints = new THREE.Points(rejectedGeometry, rejectedMaterial);
rejectedPoints.renderOrder = 1;
scene.add(rejectedPoints);
rejectedGeometryRef.current = rejectedGeometry;
rejectedMaterialRef.current = rejectedMaterial;
const selectedGeometry = new THREE.BufferGeometry();
const selectedMaterial = new THREE.PointsMaterial({
color: tokenColor(host, "--nodedc-accent-rgb", [247, 248, 244]),
map: pointTexture,
alphaTest: 0.04,
size: 10.5,
sizeAttenuation: false,
transparent: true,
opacity: 1,
depthTest: false,
depthWrite: false,
});
const selectedPoints = new THREE.Points(selectedGeometry, selectedMaterial);
selectedPoints.renderOrder = 3;
scene.add(selectedPoints);
selectedGeometryRef.current = selectedGeometry;
selectedMaterialRef.current = selectedMaterial;
const grid = new THREE.GridHelper(
10,
20,
tokenColor(host, "--nodedc-text-muted", [96, 99, 106]),
tokenColor(host, "--nodedc-glass-outline", [48, 50, 56]),
);
const gridMaterials = Array.isArray(grid.material)
? grid.material
: [grid.material];
gridMaterials.forEach((material) => {
material.transparent = true;
material.opacity = 0.16;
material.depthWrite = false;
});
gridRef.current = grid;
scene.add(grid);
const sensorMarkerGeometry = new THREE.RingGeometry(0.08, 0.12, 32);
const sensorMarkerMaterial = new THREE.MeshBasicMaterial({
color: tokenColor(host, "--nodedc-text-secondary", [185, 187, 192]),
transparent: true,
opacity: 0.64,
side: THREE.DoubleSide,
depthWrite: false,
});
const sensorMarker = new THREE.Mesh(
sensorMarkerGeometry,
sensorMarkerMaterial,
);
sensorMarker.rotation.x = -Math.PI / 2;
sensorMarker.renderOrder = 3;
scene.add(sensorMarker);
const resize = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
};
const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(host);
resize();
let animationFrame = 0;
const render = () => {
animationFrame = window.requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
};
render();
return () => {
window.cancelAnimationFrame(animationFrame);
resizeObserver.disconnect();
controls.dispose();
contextGeometry.dispose();
contextMaterial.dispose();
rejectedGeometry.dispose();
rejectedMaterial.dispose();
selectedGeometry.dispose();
selectedMaterial.dispose();
grid.geometry.dispose();
gridMaterials.forEach((material) => material.dispose());
sensorMarkerGeometry.dispose();
sensorMarkerMaterial.dispose();
pointTexture.dispose();
renderer.dispose();
renderer.domElement.remove();
contextGeometryRef.current = null;
rejectedGeometryRef.current = null;
selectedGeometryRef.current = null;
contextMaterialRef.current = null;
rejectedMaterialRef.current = null;
selectedMaterialRef.current = null;
cameraRef.current = null;
controlsRef.current = null;
gridRef.current = null;
};
}, []);
useEffect(() => {
const host = hostRef.current;
const contextGeometry = contextGeometryRef.current;
const rejectedGeometry = rejectedGeometryRef.current;
const selectedGeometry = selectedGeometryRef.current;
const contextMaterial = contextMaterialRef.current;
const rejectedMaterial = rejectedMaterialRef.current;
const selectedMaterial = selectedMaterialRef.current;
const camera = cameraRef.current;
const controls = controlsRef.current;
const grid = gridRef.current;
if (
!host
|| !contextGeometry
|| !rejectedGeometry
|| !selectedGeometry
|| !contextMaterial
|| !rejectedMaterial
|| !selectedMaterial
|| !camera
|| !controls
|| !grid
) return;
const contextPositions = toMapScenePositions(
detail.projection.pointsMapXyzM,
detail.pose.positionMapXyzM,
);
const selectedPositions = toMapScenePositions(
detail.selected.pointsMapXyzM,
detail.pose.positionMapXyzM,
);
const selectedIndices = new Set(detail.selected.sourceIndices);
const rejectedPointsMap = detail.candidate.pointsMapXyzM.filter(
(_point, index) => !selectedIndices.has(
detail.candidate.sourceIndices[index] ?? -1,
),
);
const rejectedPositions = toMapScenePositions(
rejectedPointsMap,
detail.pose.positionMapXyzM,
);
contextGeometry.setAttribute(
"position",
new THREE.BufferAttribute(contextPositions, 3),
);
rejectedGeometry.setAttribute(
"position",
new THREE.BufferAttribute(rejectedPositions, 3),
);
selectedGeometry.setAttribute(
"position",
new THREE.BufferAttribute(selectedPositions, 3),
);
contextGeometry.computeBoundingSphere();
rejectedGeometry.computeBoundingSphere();
selectedGeometry.computeBoundingSphere();
rejectedMaterial.color.copy(
tokenColor(host, "--nodedc-warning-rgb", [255, 209, 102]),
);
selectedMaterial.color.copy(
tokenColor(host, "--nodedc-accent-rgb", [247, 248, 244]),
);
const evidencePositions = selectedPositions.length || rejectedPositions.length
? new Float32Array([...rejectedPositions, ...selectedPositions])
: contextPositions;
const evidenceBounds = boundsFromPositions(evidencePositions);
const contextBounds = boundsFromPositions(contextPositions);
const evidenceView = evidenceViewFromPositions(evidencePositions);
const target = evidenceView.target;
const evidenceRadius = evidenceView.radius;
const evidenceSize = evidenceBounds.isEmpty()
? new THREE.Vector3(1, 1, 1)
: evidenceBounds.getSize(new THREE.Vector3());
const contextSize = contextBounds.isEmpty()
? evidenceSize
: contextBounds.getSize(new THREE.Vector3());
const contextRadius = Math.max(contextSize.length() / 2, evidenceRadius);
const distance = Math.max(evidenceRadius * 2.45, 2.3);
viewTargetRef.current.copy(target);
viewDistanceRef.current = distance;
controls.target.copy(target);
camera.position.set(
target.x + distance * 0.86,
target.y + distance * 0.52,
target.z + distance * 0.86,
);
camera.near = Math.max(distance / 2_000, 0.005);
camera.far = Math.max(contextRadius * 12, distance * 40, 120);
camera.updateProjectionMatrix();
controls.maxDistance = Math.max(contextRadius * 5, distance * 5, 40);
controls.update();
const contextHeights: number[] = [];
for (let offset = 1; offset < contextPositions.length; offset += 3) {
contextHeights.push(contextPositions[offset]);
}
const groundHeight = percentile(contextHeights, 0.04);
const gridSize = THREE.MathUtils.clamp(evidenceRadius * 7, 8, 48);
grid.position.set(target.x, groundHeight, target.z);
grid.scale.setScalar(gridSize / 10);
}, [detail]);
const resetCamera = () => {
const camera = cameraRef.current;
const controls = controlsRef.current;
if (!camera || !controls) return;
const target = viewTargetRef.current;
const distance = viewDistanceRef.current;
controls.target.copy(target);
camera.position.set(
target.x + distance * 0.86,
target.y + distance * 0.52,
target.z + distance * 0.86,
);
controls.update();
};
const selectedIndices = new Set(detail.selected.sourceIndices);
const rejectedCount = detail.candidate.sourceIndices.filter(
(sourceIndex) => !selectedIndices.has(sourceIndex),
).length;
return (
<div className="e30-evidence-scene" data-testid="e30-evidence-3d">
<div ref={hostRef} className="e30-evidence-scene__viewport">
{renderError ? (
<p className="e30-evidence-scene__error">{renderError}</p>
) : null}
</div>
<div className="e30-evidence-scene__toolbar">
<Button
variant="secondary"
size="compact"
icon={<Icon name="refresh" size={14} />}
onClick={resetCamera}
>
Сбросить ракурс
</Button>
<div className="e30-evidence-scene__gestures" aria-label="Управление 3D-сценой">
<span>ЛКМ · вращение</span>
<span>Колесо · масштаб</span>
<span>ПКМ · панорама</span>
</div>
</div>
<div className="e30-evidence-scene__legend" aria-label="Легенда 3D-доказательства">
<span data-point="context">
Контекст · {detail.projection.pointsMapXyzM.length}
</span>
<span data-point="rejected">Отклонено · {rejectedCount}</span>
<span data-point="selected">
Выбрано E29 · {detail.selected.pointsMapXyzM.length}
</span>
</div>
</div>
);
}
@@ -0,0 +1,215 @@
import { useEffect, useRef, useState } from "react";
import type { E30ReviewItemDetail } from "../core/laboratory/e30Review";
function tokenColor(host: HTMLElement, token: string, fallback: string): string {
return getComputedStyle(host).getPropertyValue(token).trim() || fallback;
}
function tokenRgb(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
alpha = 1,
): string {
const value = getComputedStyle(host).getPropertyValue(token).trim();
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3 ? channels : fallback;
return `rgb(${red} ${green} ${blue} / ${alpha})`;
}
function depthColor(
depth: number,
minimumDepth: number,
maximumDepth: number,
): string {
const span = Math.max(maximumDepth - minimumDepth, 0.001);
const position = Math.min(1, Math.max(0, (depth - minimumDepth) / span));
const hue = 220 - position * 205;
return `hsl(${hue} 88% 62% / 0.78)`;
}
export function E30EvidenceProjection({
detail,
projectionWidth,
projectionHeight,
pointLayerVisible,
}: {
detail: E30ReviewItemDetail;
projectionWidth: number;
projectionHeight: number;
pointLayerVisible: boolean;
}) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [frameState, setFrameState] = useState<
"loading" | "ready" | "unavailable"
>(detail.cameraFrame ? "loading" : "unavailable");
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas?.getContext("2d");
const host = canvas?.parentElement;
if (!canvas || !context || !host) return;
let cancelled = false;
const canvasWidth = 1_200;
const canvasHeight = Math.round(
canvasWidth * projectionHeight / projectionWidth,
);
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const scaleX = canvasWidth / projectionWidth;
const scaleY = canvasHeight / projectionHeight;
const depths = detail.projection.depthM.filter(Number.isFinite);
const minimumDepth = depths.length ? Math.min(...depths) : 0;
const maximumDepth = depths.length ? Math.max(...depths) : 1;
const draw = (image: HTMLImageElement | null) => {
if (cancelled) return;
context.clearRect(0, 0, canvasWidth, canvasHeight);
if (image) {
context.drawImage(image, 0, 0, canvasWidth, canvasHeight);
if (pointLayerVisible) {
context.fillStyle = "rgb(0 0 0 / 0.08)";
context.fillRect(0, 0, canvasWidth, canvasHeight);
}
} else {
context.fillStyle = tokenColor(host, "--nodedc-canvas", "#050506");
context.fillRect(0, 0, canvasWidth, canvasHeight);
}
if (pointLayerVisible) {
detail.projection.pixelsXy.forEach(([sourceX, sourceY], index) => {
if (
sourceX < 0
|| sourceX > projectionWidth
|| sourceY < 0
|| sourceY > projectionHeight
) return;
const selected = detail.projection.selectedMask[index] === 1;
const candidate = detail.projection.candidateMask[index] === 1;
const x = sourceX * scaleX;
const y = sourceY * scaleY;
context.fillStyle = selected
? tokenRgb(host, "--nodedc-accent-rgb", [247, 248, 244])
: candidate
? tokenRgb(host, "--nodedc-warning-rgb", [255, 209, 102])
: depthColor(
detail.projection.depthM[index] ?? minimumDepth,
minimumDepth,
maximumDepth,
);
context.beginPath();
context.arc(
x,
y,
selected ? 6.5 : candidate ? 4.25 : 2.1,
0,
Math.PI * 2,
);
context.fill();
});
}
const bbox = detail.snapshot.bboxXyxy;
if (bbox) {
context.strokeStyle = tokenRgb(
host,
detail.stratum === "conflict"
? "--nodedc-danger-rgb"
: "--nodedc-accent-rgb",
detail.stratum === "conflict"
? [255, 98, 92]
: [247, 248, 244],
);
context.lineWidth = 2.5;
context.strokeRect(
bbox[0] * scaleX,
bbox[1] * scaleY,
(bbox[2] - bbox[0]) * scaleX,
(bbox[3] - bbox[1]) * scaleY,
);
}
};
if (!detail.cameraFrame) {
setFrameState("unavailable");
draw(null);
return () => {
cancelled = true;
};
}
setFrameState("loading");
const image = new Image();
image.decoding = "async";
image.onload = () => {
if (cancelled) return;
if (
image.naturalWidth !== detail.cameraFrame?.width
|| image.naturalHeight !== detail.cameraFrame?.height
) {
setFrameState("unavailable");
draw(null);
return;
}
setFrameState("ready");
draw(image);
};
image.onerror = () => {
if (cancelled) return;
setFrameState("unavailable");
draw(null);
};
image.src = detail.cameraFrame.url;
return () => {
cancelled = true;
image.onload = null;
image.onerror = null;
image.src = "";
};
}, [detail, pointLayerVisible, projectionHeight, projectionWidth]);
const rejectedCount = detail.projection.candidateMask.reduce(
(count, candidate, index) => (
count
+ Number(
candidate === 1
&& detail.projection.selectedMask[index] !== 1,
)
),
0,
);
return (
<div className="e30-projection-scene" data-testid="e30-evidence-camera">
<canvas
ref={canvasRef}
aria-label={pointLayerVisible
? "Камерный кадр с проекцией LiDAR и выбранным наблюдением E30"
: "Исходный камерный кадр с рамкой наблюдения E30"}
/>
{frameState !== "ready" ? (
<div className="e30-projection-scene__state" role="status">
{frameState === "loading"
? "Проверяем точный кадр камеры…"
: "Точный кадр камеры не материализован"}
</div>
) : null}
{pointLayerVisible ? (
<div
className="e30-evidence-scene__legend"
aria-label="Легенда camera-LiDAR доказательства"
>
<span data-point="depth">
LiDAR · глубина · {detail.projection.pointsMapXyzM.length}
</span>
<span data-point="rejected">Кандидаты · {rejectedCount}</span>
<span data-point="selected">
Выбрано E29 · {detail.selected.pointsMapXyzM.length}
</span>
</div>
) : null}
</div>
);
}
@@ -0,0 +1,283 @@
import { useEffect, useState } from "react";
import {
Button,
ConfirmationModal,
Select,
StatusBadge,
TextAreaField,
} from "@nodedc/ui-react";
import {
createOrResumeE30HumanReview,
finalizeE30HumanReview,
saveE30HumanReviewDecision,
type E30ExceptionDisposition,
type E30HumanReviewDraft,
} from "../core/laboratory/e30HumanReview";
import type { E30EngineeringGeneration } from "../core/laboratory/e30Engineering";
import type {
E30ReviewItemDetail,
E30ReviewResult,
} from "../core/laboratory/e30Review";
const DISPOSITION_OPTIONS: readonly {
value: E30ExceptionDisposition;
label: string;
}[] = [
{ value: "object-present", label: "Объект есть" },
{ value: "background-or-noise", label: "Фон или шум" },
{ value: "insufficient-evidence", label: "Недостаточно данных" },
];
const FALLBACK_REVIEW_PROMPT = {
question: "Белый кластер — самостоятельное физическое препятствие?",
focus: (
"Сопоставьте выбранные белые точки с исходным кадром и решите, "
+ "принадлежат ли они занятой геометрии реального объекта."
),
effects: {
"object-present": "Сохранить кластер как занятую геометрию.",
"background-or-noise": "Исключить кластер как фон или шум.",
"insufficient-evidence": "Оставить кейс неизвестным без настройки порогов.",
},
} as const;
function decisionFor(
review: E30HumanReviewDraft | null,
itemId: string | undefined,
) {
return itemId
? review?.decisions.find((decision) => decision.itemId === itemId) ?? null
: null;
}
export function E30HumanReviewPanel({
result,
generation,
item,
review,
onReviewChange,
onDecisionSaved,
}: {
result: E30ReviewResult;
generation: E30EngineeringGeneration;
item: E30ReviewItemDetail | null;
review: E30HumanReviewDraft | null;
onReviewChange: (review: E30HumanReviewDraft) => void;
onDecisionSaved: (review: E30HumanReviewDraft) => void;
}) {
const [disposition, setDisposition] =
useState<E30ExceptionDisposition>("object-present");
const [notes, setNotes] = useState("");
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const [finalizeOpen, setFinalizeOpen] = useState(false);
const currentDecision = decisionFor(review, item?.itemId);
const reviewPrompt = generation.humanExceptions.find(
(exception) => exception.itemId === item?.itemId,
)?.reviewPrompt ?? FALLBACK_REVIEW_PROMPT;
useEffect(() => {
setDisposition(currentDecision?.disposition ?? "object-present");
setNotes(currentDecision?.notes ?? "");
setError(null);
}, [currentDecision, item?.itemId]);
const begin = async () => {
if (pending) return;
setPending(true);
setError(null);
try {
onReviewChange(await createOrResumeE30HumanReview(
result.resultId,
generation.generationId,
));
} catch (caught) {
setError(
caught instanceof Error ? caught.message : "Проверка недоступна.",
);
} finally {
setPending(false);
}
};
const save = async () => {
if (!review || !item || review.state !== "active" || pending) return;
setPending(true);
setError(null);
try {
const next = await saveE30HumanReviewDecision(
result.resultId,
generation.generationId,
review.draftId,
item.itemId,
{
expectedRevision: review.revision,
idempotencyKey: `ui-${crypto.randomUUID()}`,
disposition,
notes: notes.trim() || null,
},
);
onReviewChange(next);
onDecisionSaved(next);
} catch (caught) {
setError(
caught instanceof Error ? caught.message : "Решение не сохранено.",
);
} finally {
setPending(false);
}
};
const finalize = async () => {
if (!review || review.state !== "active") return;
setError(null);
try {
const finalized = await finalizeE30HumanReview(
result.resultId,
generation.generationId,
review.draftId,
review.revision,
);
onReviewChange(finalized.draft);
setFinalizeOpen(false);
} catch (caught) {
setError(
caught instanceof Error ? caught.message : "Проверка не зафиксирована.",
);
throw caught;
}
};
if (!review) {
return (
<section className="e30-human-review" aria-label="Проверка исключений">
<header className="e30-human-review__header">
<div>
<span className="section-eyebrow">ТРЕБУЕТСЯ РЕШЕНИЕ</span>
<h3>{reviewPrompt.question}</h3>
<p>{reviewPrompt.focus}</p>
</div>
<StatusBadge tone="warning">
0 / {generation.summary.humanExceptionCount}
</StatusBadge>
</header>
<div className="e30-human-review__actions">
<span>
Решение изменит только отдельную A3-коррекцию; A2 останется
неизменным.
</span>
<Button
variant="primary"
disabled={pending}
onClick={() => void begin()}
>
{pending ? "Открываем…" : "Начать проверку"}
</Button>
</div>
{error ? (
<p className="e30-human-review__error" role="alert">{error}</p>
) : null}
</section>
);
}
const finalized = review.state === "finalized";
return (
<section className="e30-human-review" aria-label="Проверка исключений">
<header className="e30-human-review__header">
<div>
<span className="section-eyebrow">ПРОВЕРКА ИСКЛЮЧЕНИЙ</span>
<h3>
{finalized ? "Проверка зафиксирована" : reviewPrompt.question}
</h3>
{!finalized ? <p>{reviewPrompt.focus}</p> : null}
</div>
<StatusBadge tone={finalized ? "success" : "accent"}>
{review.reviewedItemCount} / {review.itemCount}
</StatusBadge>
</header>
{finalized ? (
<p>
Все спорные кадры получили отдельное человеческое решение.
Исходные доказательства сохранены без изменений.
</p>
) : item ? (
<>
<div className="e30-human-review__form">
<div className="e30-human-review__field">
<span>Решение</span>
<Select
label="Что видно на выбранном кадре"
value={disposition}
options={[...DISPOSITION_OPTIONS]}
variant="split"
menuWidth="anchor"
disabled={pending}
onChange={setDisposition}
/>
</div>
<TextAreaField
label="Комментарий"
hint="необязательно"
value={notes}
rows={2}
maxLength={2_000}
disabled={pending}
onChange={(event) => setNotes(event.currentTarget.value)}
/>
</div>
<div className="e30-human-review__impact">
<span>Что изменится после решения</span>
<strong>{reviewPrompt.effects[disposition]}</strong>
</div>
<div className="e30-human-review__actions">
<span>
{currentDecision
? "Этот кадр уже решён — его можно пересмотреть."
: `${review.remainingItemCount} решений осталось.`}
</span>
<Button
variant="primary"
disabled={pending}
onClick={() => void save()}
>
{pending
? "Сохраняем…"
: currentDecision
? "Обновить кадр"
: "Сохранить кадр"}
</Button>
<Button
variant="secondary"
disabled={review.remainingItemCount !== 0 || pending}
onClick={() => setFinalizeOpen(true)}
>
Зафиксировать проверку
</Button>
</div>
</>
) : null}
{error ? (
<p className="e30-human-review__error" role="alert">{error}</p>
) : null}
<ConfirmationModal
open={finalizeOpen}
title="Зафиксировать проверку?"
description={(
<p>
Будет создан неизменяемый набор из {review.itemCount} решений.
После фиксации их нельзя будет изменить.
</p>
)}
confirmLabel="Зафиксировать"
pendingLabel="Фиксируем…"
onClose={() => setFinalizeOpen(false)}
onConfirm={finalize}
/>
</section>
);
}
@@ -0,0 +1,362 @@
import { useEffect, useState } from "react";
import {
Button,
GlassSurface,
Icon,
SegmentedControl,
StatusBadge,
} from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../components/laboratory/LaboratoryEvidenceViewer";
import { E30EvidenceTelemetry } from "../components/laboratory/E30EvidenceTelemetry";
import { E30EngineeringGenerationSummary } from "../components/laboratory/E30EngineeringGenerationSummary";
import {
fetchE30EngineeringCatalog,
fetchE30EngineeringExceptions,
type E30EngineeringGeneration,
} from "../core/laboratory/e30Engineering";
import type { E30HumanReviewDraft } from "../core/laboratory/e30HumanReview";
import {
E30_STRATA,
fetchE30ReviewItemDetail,
fetchE30ReviewItems,
type E30ReviewItem,
type E30ReviewItemDetail,
type E30ReviewResult,
type E30Stratum,
} from "../core/laboratory/e30Review";
import { formatNumber } from "../presentation";
import { E30EvidencePointCloud } from "./E30EvidencePointCloud";
import { E30EvidenceProjection } from "./E30EvidenceProjection";
import { E30HumanReviewPanel } from "./E30HumanReviewPanel";
type E30EvidenceMode = "camera" | "3d";
type E30Filter = E30Stratum | "review";
const FILTER_LABELS: Record<E30Filter, string> = {
conflict: "Конфликт",
agree: "Согласовано",
"camera-only": "Только камера",
unknown: "Неизвестно",
"geometry-only": "Только геометрия",
review: "Проверка",
};
const FILTERS: readonly E30Filter[] = [...E30_STRATA, "review"];
function formatSeconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`;
}
function itemTitle(item: E30ReviewItem): string {
if (item.snapshot.label) return item.snapshot.label;
return item.locatorKind === "geometry-only-cluster"
? "Геометрический кластер"
: "Семантическое наблюдение";
}
function evidenceRange(item: E30ReviewItem): string {
const value = item.snapshot.rangeM ?? item.snapshot.nearestRangeM;
return value === null
? "Дальность недоступна"
: `${value.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} м`;
}
export function E30ReviewWorkspace({
result,
}: {
result: E30ReviewResult;
}) {
const [filter, setFilter] = useState<E30Filter>("conflict");
const [items, setItems] = useState<readonly E30ReviewItem[]>([]);
const [itemTotal, setItemTotal] = useState(result.stratumCounts.conflict);
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
const [detail, setDetail] = useState<E30ReviewItemDetail | null>(null);
const [itemsLoading, setItemsLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [evidenceMode, setEvidenceMode] = useState<E30EvidenceMode>("camera");
const [pointLayerVisible, setPointLayerVisible] = useState(true);
const [viewerExpanded, setViewerExpanded] = useState(false);
const [engineeringGeneration, setEngineeringGeneration] =
useState<E30EngineeringGeneration | null>(null);
const [engineeringLoading, setEngineeringLoading] = useState(true);
const [humanReview, setHumanReview] =
useState<E30HumanReviewDraft | null>(null);
useEffect(() => {
if (filter === "review" && !engineeringGeneration) {
setItems([]);
setItemTotal(0);
setSelectedItemId(null);
setItemsLoading(engineeringLoading);
return;
}
const controller = new AbortController();
setItemsLoading(true);
setError(null);
setDetail(null);
const request = filter === "review"
? fetchE30EngineeringExceptions(
result.resultId,
engineeringGeneration!.generationId,
{ signal: controller.signal },
)
: fetchE30ReviewItems(result.resultId, filter, {
signal: controller.signal,
});
void request.then((next) => {
setItems(next.items);
setItemTotal(next.total);
setSelectedItemId((current) => (
next.items.some((item) => item.itemId === current)
? current
: next.items[0]?.itemId ?? null
));
}).catch((caught: unknown) => {
if (controller.signal.aborted) return;
setItems([]);
setSelectedItemId(null);
setError(caught instanceof Error ? caught.message : "Выборка E30 недоступна.");
}).finally(() => {
if (!controller.signal.aborted) setItemsLoading(false);
});
return () => controller.abort();
}, [
engineeringGeneration,
engineeringLoading,
filter,
result.resultId,
]);
useEffect(() => {
const controller = new AbortController();
setEngineeringLoading(true);
setEngineeringGeneration(null);
void fetchE30EngineeringCatalog(result.resultId, {
signal: controller.signal,
}).then((catalog) => {
setEngineeringGeneration(catalog.items[0] ?? null);
}).catch(() => {
if (!controller.signal.aborted) setEngineeringGeneration(null);
}).finally(() => {
if (!controller.signal.aborted) setEngineeringLoading(false);
});
return () => controller.abort();
}, [result.resultId]);
useEffect(() => {
if (!selectedItemId) {
setDetail(null);
return;
}
const controller = new AbortController();
setDetailLoading(true);
setError(null);
void fetchE30ReviewItemDetail(result.resultId, selectedItemId, {
signal: controller.signal,
}).then(setDetail).catch((caught: unknown) => {
if (controller.signal.aborted) return;
setDetail(null);
setError(caught instanceof Error ? caught.message : "Доказательство E30 недоступно.");
}).finally(() => {
if (!controller.signal.aborted) setDetailLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedItemId]);
const selectItem = (item: E30ReviewItem) => {
setSelectedItemId(item.itemId);
};
const advanceAfterDecision = (next: E30HumanReviewDraft) => {
const resolved = new Set(next.decisions.map((decision) => decision.itemId));
const currentIndex = items.findIndex((item) => item.itemId === selectedItemId);
const ordered = [
...items.slice(currentIndex + 1),
...items.slice(0, currentIndex + 1),
];
const unresolved = ordered.find((item) => !resolved.has(item.itemId));
if (unresolved) setSelectedItemId(unresolved.itemId);
};
return (
<GlassSurface
className="e30-review-workspace"
tone="soft"
padding="md"
materialRim={false}
role="region"
aria-label="Рабочее место ревью E30"
>
<header className="e30-review-workspace__header">
<div>
<span className="section-eyebrow">CAMERA-BACKED REVIEW SUBSTRATE</span>
<h2>A2 evidence · A3 engineering audit</h2>
<p>
Точный camera frame, LiDAR-проекция и синхронный 3D сохраняют A2
неизменяемым. A3 выпускает отдельные решения с явным provenance.
</p>
</div>
<StatusBadge tone={result.cameraEvidenceAvailable ? "accent" : "warning"}>
{result.cameraEvidenceAvailable
? "Camera evidence привязано"
: "Camera evidence отсутствует"}
</StatusBadge>
</header>
{engineeringGeneration ? (
<E30EngineeringGenerationSummary
generation={engineeringGeneration}
/>
) : null}
<SegmentedControl
className="e30-review-workspace__strata"
value={filter}
label="Группа E30"
items={FILTERS.map((value) => ({
value,
label: `${FILTER_LABELS[value]} · ${formatNumber(
value === "review"
? engineeringGeneration?.summary.humanExceptionCount ?? 0
: result.stratumCounts[value],
0,
)}`,
}))}
onChange={setFilter}
/>
<div className="e30-review-workspace__body">
<aside className="e30-review-workspace__items" aria-label="Кейсы выбранной страты">
<header>
<span>{FILTER_LABELS[filter]}</span>
<small>показано {items.length} из {itemTotal}</small>
</header>
<div className="e30-review-workspace__item-list">
{itemsLoading ? (
<div className="e30-review-workspace__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Проверяем индекс</span>
</div>
) : items.length ? items.map((item) => (
<Button
key={item.itemId}
variant="secondary"
size="compact"
width="full"
className="e30-review-workspace__item"
data-active={item.itemId === selectedItemId ? "true" : undefined}
aria-pressed={item.itemId === selectedItemId}
onClick={() => selectItem(item)}
>
<span>{itemTitle(item)}</span>
<strong>Кадр {formatNumber(item.sourceFrameIndex, 0)}</strong>
<small>
{formatSeconds(item.sessionSeconds)}
{" · "}
{evidenceRange(item)}
{filter === "review"
&& humanReview?.decisions.some(
(decision) => decision.itemId === item.itemId,
)
? " · Решено"
: ""}
</small>
</Button>
)) : (
<div className="e30-review-workspace__state">
<Icon name="database" size={18} />
<span>В этой страте кейсов нет</span>
</div>
)}
</div>
</aside>
<main className="e30-review-workspace__detail">
{detailLoading ? (
<div className="e30-review-workspace__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Проверяем camera-LiDAR доказательство</span>
</div>
) : error || !detail ? (
<div className="e30-review-workspace__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Выберите кейс."}</span>
</div>
) : (
<>
<header className="e30-review-workspace__case">
<div>
<span className="section-eyebrow">{detail.reviewKey}</span>
<h3>{itemTitle(detail)}</h3>
<p>
{detail.snapshot.geometryReason ?? "Независимый geometry-only слой"}
</p>
</div>
<StatusBadge tone={detail.stratum === "conflict" ? "danger" : "neutral"}>
{FILTER_LABELS[detail.stratum]}
</StatusBadge>
</header>
<div className="e30-review-evidence">
<LaboratoryEvidenceViewer
label="Доказательство E30"
mode={evidenceMode}
modes={[
{ value: "camera", label: "Камера" },
{ value: "3d", label: "3D" },
]}
expanded={viewerExpanded}
onModeChange={setEvidenceMode}
onExpandedChange={setViewerExpanded}
actions={evidenceMode === "camera" ? (
<Button
size="compact"
variant={pointLayerVisible ? "primary" : "secondary"}
icon={<Icon name="sliders" size={16} />}
aria-pressed={pointLayerVisible}
onClick={() => setPointLayerVisible((visible) => !visible)}
>
LiDAR
</Button>
) : undefined}
overlay={(
<E30EvidenceTelemetry
detail={detail}
mode={evidenceMode}
/>
)}
>
{evidenceMode === "camera" ? (
<E30EvidenceProjection
detail={detail}
projectionWidth={result.projection.width}
projectionHeight={result.projection.height}
pointLayerVisible={pointLayerVisible}
/>
) : (
<E30EvidencePointCloud detail={detail} />
)}
</LaboratoryEvidenceViewer>
</div>
</>
)}
</main>
</div>
{!detailLoading
&& !error
&& filter === "review"
&& engineeringGeneration
&& detail ? (
<E30HumanReviewPanel
result={result}
generation={engineeringGeneration}
item={detail}
review={humanReview}
onReviewChange={setHumanReview}
onDecisionSaved={advanceAfterDecision}
/>
) : null}
</GlassSurface>
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
import type { ComponentType } from "react";
import type { ObservationSessionReplayCallbacks } from "../components/ObservationSessionSelect";
import type {
DeviceModelDefinition,
DevicePluginConnectionProps,
} from "../core/device-plugins/contracts";
import type { ObservationLayoutController } from "../core/observation/useObservationLayout";
import type { ObservationSessionReplayLaunch } from "../core/observation/sessionArchive";
import type { RecordedSessionAdmissionController } from "../core/observation/useRecordedSessionAdmission";
import type {
BackendStatus,
MissionRuntimeState,
} from "../core/runtime/contracts";
import type { WorkspaceDefinition } from "../productModel";
import type { SceneSettings } from "../sceneSettings";
export interface WorkspaceNavigation {
openView: (viewId: string) => void;
openSource: () => void;
openDisplay: () => void;
openLayers: () => void;
activateAutomaticSpatialSource: () => void;
}
export interface WorkspaceRendererProps {
definition: WorkspaceDefinition;
state: MissionRuntimeState | null;
backendStatus: BackendStatus;
sourceUrl: string;
requestedPlaybackSeconds?: number | null;
recordedReplay: ObservationSessionReplayLaunch | null;
recordedSessionAdmission: RecordedSessionAdmissionController | null;
sceneSettings: SceneSettings;
accumulationSeconds: number;
onAccumulationChange: (value: number) => void;
onAccumulationCommit: () => void;
livePerceptionLayers: {
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
};
onLivePerceptionLayersChange: (next: {
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
}) => void;
observationLayout: ObservationLayoutController;
deviceLabel: string | null;
navigation: WorkspaceNavigation;
spatialControls: {
View: ComponentType<DevicePluginConnectionProps>;
model: DeviceModelDefinition;
} | null;
sessionArchive: ObservationSessionReplayCallbacks & {
disabled: boolean;
blockedReason: string | null;
};
}
@@ -0,0 +1,882 @@
import {
useEffect,
useMemo,
useState,
type ComponentType,
} from "react";
import { Icon, StatusBadge } from "@nodedc/ui-react";
import {
LaboratoryEvidence,
LaboratorySelector,
LaboratorySummary,
LaboratoryWorkTemplate,
type LaboratoryMethod,
type LaboratoryMethodComponent,
type LaboratoryOption,
} from "../../components/laboratory/LaboratoryPresentation";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
import { useObservationSessions } from "../../core/observation/useObservationSessions";
import {
fetchE29EvidenceCatalog,
fetchE29EvidenceFrame,
type E29EvidenceFrame,
type E29EvidenceResult,
} from "../../core/laboratory/e29Evidence";
import {
fetchE30ReviewCatalog,
type E30ReviewResult,
} from "../../core/laboratory/e30Review";
import {
fetchLidarLocalSurfaces,
type LidarLocalSurfaceModel,
} from "../../core/lidar/localSurface";
import { formatNumber } from "../../presentation";
import { E30ReviewWorkspace } from "../E30ReviewWorkspace";
import { LidarQualityWorkspace } from "../LidarQualityWorkspace";
import type { WorkspaceRendererProps } from "../contracts";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
type LaboratoryProfileId = "sensor-fusion" | "published-perception";
type LaboratoryWorkId =
| "e28-local-surface"
| "e29-camera-geometry"
| "e30-evidence-review"
| `session:${string}`;
function digestFromContentId(value: string | null | undefined): string | null {
const digest = value?.split("-").at(-1) ?? "";
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
}
function publishedLaboratoryMethod(
session: ObservationSessionSummary,
): LaboratoryMethod {
const method = session.lab?.provenance.method;
if (method && typeof method === "object" && !Array.isArray(method)) {
const value = method as Record<string, unknown>;
const rawComponents = Array.isArray(value.components) ? value.components : [];
const components: LaboratoryMethodComponent[] = rawComponents.flatMap((component) => {
if (!component || typeof component !== "object" || Array.isArray(component)) return [];
const item = component as Record<string, unknown>;
const kind = item.kind;
if (
kind !== "source"
&& kind !== "tool"
&& kind !== "model"
&& kind !== "algorithm"
&& kind !== "runtime"
) return [];
if (
typeof item.name !== "string"
|| typeof item.version !== "string"
|| typeof item.role !== "string"
) return [];
return [{
kind: kind as LaboratoryMethodComponent["kind"],
name: item.name,
version: item.version,
role: item.role,
identitySha256: typeof item.identity_sha256 === "string"
? item.identity_sha256
: null,
}];
});
const executionClass = value.execution_class;
const completeness = value.completeness;
if (
components.length
&& typeof value.pipeline_id === "string"
&& (
executionClass === "deterministic"
|| executionClass === "ai-inference"
|| executionClass === "hybrid"
)
&& (completeness === "complete" || completeness === "legacy-partial")
) {
return {
completeness,
executionClass,
pipelineId: value.pipeline_id,
components,
};
}
}
const resultKind = session.lab?.resultKind ?? "unknown";
const algorithmNames: Record<string, string> = {
"e10-integrated-perception": "Camera semantics + LiDAR metric fusion",
"e21-realtime-envelope": "Bounded real-time perception replay",
"e22-temporal-stability": "Temporal 2D/3D/semantic stabilization",
"e23-inline-temporal-stability": "Inline warm-worker stabilization",
"e24-world-motion": "World-frame motion tracking",
"e25-persistent-support-motion": "Persistent occupied-support tracking",
"e26-camera-ego-motion-fusion": "KB4 ego-motion + persistent LiDAR support",
};
return {
completeness: "legacy-partial",
executionClass: "hybrid",
pipelineId: resultKind,
components: [
{
kind: "source",
name: session.lab?.sourceResultId ?? session.lab?.sourceSessionId ?? session.id,
version: "immutable source evidence",
role: "read-only input",
identitySha256: digestFromContentId(session.lab?.sourceResultId),
},
{
kind: "algorithm",
name: algorithmNames[resultKind] ?? resultKind,
version: resultKind,
role: "laboratory derivative",
identitySha256: session.lab?.configSha256 ?? null,
},
],
};
}
function formatSeconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`;
}
function E29LaboratoryResult({
props,
rigLabel,
result,
sourceSession,
loading,
error,
}: {
props: LaboratoryWorkspaceProps;
rigLabel: string;
result: E29EvidenceResult;
sourceSession: ObservationSessionSummary;
loading: boolean;
error: string | null;
}) {
const [selectedFrameIndex, setSelectedFrameIndex] = useState(
result.reviewFrames[0]?.frameIndex ?? 0,
);
const [frame, setFrame] = useState<E29EvidenceFrame | null>(null);
const [frameLoading, setFrameLoading] = useState(false);
const [frameError, setFrameError] = useState<string | null>(null);
const replayReady = props.recordedReplay?.sessionId === sourceSession.id;
const semantic = result.metrics.semanticObservations;
const geometryStatus = semantic.geometryStatus;
const conflicts = frame?.semanticObservations.filter(
(observation) => observation.geometryStatus === "conflict",
) ?? [];
useEffect(() => {
const controller = new AbortController();
setFrameLoading(true);
setFrameError(null);
void fetchE29EvidenceFrame(result.resultId, selectedFrameIndex, {
signal: controller.signal,
}).then((next) => {
setFrame(next);
}).catch((caught: unknown) => {
if (controller.signal.aborted) return;
setFrame(null);
setFrameError(
caught instanceof Error ? caught.message : "Кадр E29 недоступен.",
);
}).finally(() => {
if (!controller.signal.aborted) setFrameLoading(false);
});
return () => controller.abort();
}, [result.resultId, selectedFrameIndex]);
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E29 · camera-first semantics + независимая геометрия"
description="Камера сохраняет класс и идентичность объекта, а LiDAR независимо подтверждает дальность и занятую геометрию по локальной поверхности L2.6. Отсутствие точек не объявляется свободным пространством."
status="Проверенные артефакты"
statusTone="success"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · камера + LiDAR · worker D` },
{
label: "Источник",
value: `${sourceSession.label} · ${formatNumber(result.identity.frameCount, 0)} кадров`,
},
{
label: "Наблюдений",
value: formatNumber(semantic.total, 0),
},
{
label: "Контур",
value: "Read-only · hash verified",
},
]}
method={{
completeness: "complete",
executionClass: "hybrid",
pipelineId: result.identity.profileId,
components: [
{
kind: "source",
name: result.linkedEvidence.sourceResultId,
version: "camera-first semantic observations",
role: "semantic identity and class",
identitySha256: digestFromContentId(result.linkedEvidence.sourceResultId),
},
{
kind: "algorithm",
name: "Camera/LiDAR local-surface validation",
version: result.identity.profileId,
role: "range, occupied support and conflict classification",
identitySha256: result.identity.producerSha256,
},
{
kind: "model",
name: result.linkedEvidence.localSurfaceModelId,
version: "L2.6 local surface",
role: "independent metric geometry",
identitySha256: digestFromContentId(
result.linkedEvidence.localSurfaceModelId,
),
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ИСХОДНЫЕ ДАННЫЕ"
title="LiDAR, траектория и камера RAVNOVES00"
kind="recorded-replay"
resizable
>
{replayReady ? (
<props.SpatialView {...props} />
) : (
<div className="laboratory-result-pending" role="status">
{loading ? <span className="busy-indicator" aria-hidden="true" /> : <Icon name="database" size={20} />}
<strong>{loading ? "Проверяем и открываем запись" : "Исходная запись не открыта"}</strong>
<p>
{error ?? (loading
? "Viewer появится после серверной проверки неизменяемого RRD."
: "Выберите LAB E29 повторно, чтобы открыть связанный источник.")}
</p>
</div>
)}
</LaboratoryEvidence>
)}
result={(
<section className="laboratory-result-summary">
<header>
<div>
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
<h2>Camera-first контракт рассчитан, production gate не пройден</h2>
</div>
<StatusBadge tone={result.decision.productionPromotion ? "success" : "warning"}>
{result.decision.productionPromotion ? "Допущено" : "Только диагностика"}
</StatusBadge>
</header>
<div className="laboratory-result-metrics">
<div>
<span>Поддержка геометрией</span>
<strong>{formatNumber(geometryStatus.agree, 0)}</strong>
<small>{(semantic.agreementFractionOfCurrent * 100).toLocaleString("ru-RU", { maximumFractionDigits: 2 })}% current</small>
</div>
<div>
<span>Только камера</span>
<strong>{formatNumber(geometryStatus.cameraOnly, 0)}</strong>
<small>Семантика без LiDAR-подтверждения</small>
</div>
<div>
<span>Postprocess p95</span>
<strong>{result.metrics.runtime.frameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс</strong>
<small>{formatSeconds(result.metrics.runtime.buildElapsedMs / 1000)} полный build</small>
</div>
<div>
<span>Только геометрия</span>
<strong>{formatNumber(result.metrics.geometryOnlyOccupied.clusterCount, 0)}</strong>
<small>{formatNumber(result.metrics.geometryOnlyOccupied.pointCount, 0)} точек</small>
</div>
</div>
<p>{result.decision.nextGate}</p>
</section>
)}
details={(
<section className="laboratory-frame-review">
<header>
<div>
<span className="section-eyebrow">КАДРЫ С КОНФЛИКТОМ</span>
<h2>Покадровое доказательство из camera-geometry-frames.jsonl</h2>
</div>
<StatusBadge tone="warning">
{formatNumber(geometryStatus.conflict, 0)} конфликтов
</StatusBadge>
</header>
<div className="laboratory-frame-review__picker" role="list">
{result.reviewFrames.slice(0, 16).map((review) => (
<button
key={review.frameIndex}
type="button"
className={review.frameIndex === selectedFrameIndex ? "is-active" : undefined}
onClick={() => setSelectedFrameIndex(review.frameIndex)}
>
<span>Кадр {formatNumber(review.sourceFrameIndex, 0)}</span>
<small>{formatSeconds(review.sessionSeconds)} · {review.conflictCount} конфликт</small>
</button>
))}
</div>
{frameLoading ? (
<div className="laboratory-frame-review__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Читаем подтверждённый кадр</span>
</div>
) : frameError || !frame ? (
<div className="laboratory-frame-review__state" role="status">
<Icon name="database" size={18} />
<span>{frameError ?? "Кадр недоступен."}</span>
</div>
) : (
<div className="laboratory-frame-review__detail">
<div>
<span>Кадр источника</span>
<strong>{formatNumber(frame.sourceFrameIndex, 0)}</strong>
<small>{formatSeconds(frame.sessionSeconds)}</small>
</div>
<div>
<span>Семантические наблюдения</span>
<strong>{formatNumber(frame.semanticObservations.length, 0)}</strong>
<small>{formatNumber(conflicts.length, 0)} требуют разбора</small>
</div>
<div>
<span>Geometry-only компоненты</span>
<strong>{formatNumber(frame.geometryOnlyOccupied.length, 0)}</strong>
<small>Класс не назначается</small>
</div>
<div className="laboratory-frame-review__conflicts">
<span>Фактические конфликты</span>
{conflicts.length ? conflicts.map((observation) => (
<p key={observation.trackId}>
<strong>{observation.label} · track {observation.trackId}</strong>
<small>
{observation.geometryReason} · classified {observation.support.classifiedPoints}
{" · "}surface {observation.support.surfacePoints}
</small>
</p>
)) : <small>В этом кадре конфликт не найден.</small>}
</div>
</div>
)}
</section>
)}
/>
);
}
function E30LaboratoryResult({
rigLabel,
result,
sourceSession,
}: {
rigLabel: string;
result: E30ReviewResult;
sourceSession: ObservationSessionSummary;
}) {
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E30 · рабочее место evidence review"
description="A2 связывает каждый кейс E29 с точным camera frame, LiDAR-проекцией и frame-local индексами. Камера отвечает на вопрос «что видит детектор», синхронный 3D проверяет принадлежность и форму точек."
status="Camera evidence проверено"
statusTone="success"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · камера + LiDAR · A2` },
{ label: "Источник", value: sourceSession.label },
{ label: "Кейсов", value: formatNumber(result.itemCount, 0) },
{ label: "Контур", value: "Read-only · без LAB publish" },
]}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="CAMERA + LIDAR ДОКАЗАТЕЛЬСТВО"
title="Точный кадр, проекция и синхронный 3D"
kind="diagnostic-model"
>
<E30ReviewWorkspace result={result} />
</LaboratoryEvidence>
)}
/>
);
}
function formatLaboratoryMetric(value: unknown): string {
if (typeof value === "boolean") return value ? "Да" : "Нет";
if (typeof value === "number") {
return value.toLocaleString("ru-RU", { maximumFractionDigits: 3 });
}
return String(value);
}
function laboratoryMetricLabel(key: string): string {
return key
.replace(/_p95_ms$/, " · p95 мс")
.replaceAll("_", " ")
.replace(/^./, (value) => value.toLocaleUpperCase("ru-RU"));
}
function laboratorySessionTitle(session: ObservationSessionSummary): string {
const labPrefix = session.lab ? `${session.lab.labId} · ` : "";
return labPrefix && session.label.startsWith(labPrefix)
? session.label.slice(labPrefix.length)
: session.label;
}
function PublishedLaboratoryResult({
props,
session,
loading,
error,
}: {
props: LaboratoryWorkspaceProps;
session: ObservationSessionSummary;
loading: boolean;
error: string | null;
}) {
const lab = session.lab;
const metrics = Object.entries(lab?.provenance ?? {})
.filter(([key, value]) => (
(typeof value === "number" || typeof value === "boolean")
&& !key.includes("sha256")
&& !key.includes("authority")
))
.slice(0, 8);
const replayReady = props.recordedReplay?.sessionId === session.id;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title={`${lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`}
description="Опубликованная работа открывается по неизменяемой записи. Viewer показывает исходные синхронные каналы, а продуктовая выжимка — только зафиксированный метод и LAB-provenance."
status="Зафиксированный результат"
statusTone="success"
facts={[
{ label: "Тип результата", value: lab?.resultKind ?? "—" },
{ label: "Источник", value: lab?.sourceSessionId ?? session.id },
{ label: "Конфигурация", value: lab?.configSha256?.slice(0, 16) ?? "Не зафиксирована" },
{ label: "Контур", value: "Диагностика · без команд" },
]}
method={publishedLaboratoryMethod(session)}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
title="Исходная запись выбранной лабораторной работы"
kind="recorded-replay"
resizable
>
{replayReady ? (
<props.SpatialView {...props} />
) : (
<div className="laboratory-result-pending" role="status">
{loading ? <span className="busy-indicator" aria-hidden="true" /> : <Icon name="database" size={20} />}
<strong>{loading ? "Подготавливаем лабораторную запись" : "Запись не открыта"}</strong>
<p>
{error ?? (loading
? "Связанное визуальное доказательство откроется после проверки записи."
: "Выберите работу ещё раз, чтобы открыть связанное визуальное доказательство.")}
</p>
</div>
)}
</LaboratoryEvidence>
)}
result={(
<section className="laboratory-result-summary">
<header>
<div>
<span className="section-eyebrow">ЗАФИКСИРОВАННЫЕ МЕТРИКИ</span>
<h2>Результат из LAB-provenance</h2>
</div>
<StatusBadge tone={
lab?.provenance.benchmark_passed === true ? "success" : "warning"
}>
{lab?.provenance.benchmark_passed === true ? "Benchmark passed" : "Требует разбора"}
</StatusBadge>
</header>
<div className="laboratory-result-metrics">
{metrics.length ? metrics.map(([key, value]) => (
<div key={key}>
<span>{laboratoryMetricLabel(key)}</span>
<strong>{formatLaboratoryMetric(value)}</strong>
<small>Immutable provenance</small>
</div>
)) : (
<div>
<span>Метрики</span>
<strong>Не опубликованы</strong>
<small>Доступна исходная запись</small>
</div>
)}
</div>
</section>
)}
/>
);
}
export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const [profileId, setProfileId] = useState<LaboratoryProfileId>("sensor-fusion");
const [workId, setWorkId] = useState<LaboratoryWorkId>("e28-local-surface");
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
const [evidenceLoading, setEvidenceLoading] = useState(true);
const [evidenceError, setEvidenceError] = useState<string | null>(null);
const sessions = useObservationSessions({
limit: 100,
replayEnabled: props.sessionArchive.blockedReason === null,
onReplayBegin: props.sessionArchive.onReplayBegin,
onReplayAccepted: props.sessionArchive.onReplayAccepted,
onReplaySettled: props.sessionArchive.onReplaySettled,
});
const publishedWorks = useMemo(
() => sessions.items.filter((session) => (
session.lab !== null
&& session.status === "ready"
&& session.replayable
&& session.modalities.includes("point-cloud")
)),
[sessions.items],
);
const sourceSessions = useMemo(
() => new Map(sessions.items.map((session) => [session.id, session])),
[sessions.items],
);
useEffect(() => {
const controller = new AbortController();
setEvidenceLoading(true);
setEvidenceError(null);
void Promise.allSettled([
fetchLidarLocalSurfaces({ signal: controller.signal }),
fetchE29EvidenceCatalog({ signal: controller.signal }),
fetchE30ReviewCatalog({ signal: controller.signal }),
]).then(([e28, e29, e30]) => {
if (controller.signal.aborted) return;
const nextE28 = e28.status === "fulfilled" ? e28.value.items[0] ?? null : null;
const nextE29 = e29.status === "fulfilled" ? e29.value.items[0] ?? null : null;
const nextE30 = e30.status === "fulfilled" ? e30.value.items[0] ?? null : null;
setE28Model(nextE28);
setE29Result(nextE29);
setE30Result(nextE30);
const failures = [
e28.status === "rejected" ? "E28" : null,
e29.status === "rejected" ? "E29" : null,
e30.status === "rejected" ? "E30" : null,
].filter(Boolean);
setEvidenceError(
failures.length
? `${failures.join(" и ")} не прошли серверную проверку и скрыты.`
: null,
);
}).finally(() => {
if (!controller.signal.aborted) setEvidenceLoading(false);
});
return () => controller.abort();
}, []);
const rigLabel = useMemo(() => {
if (props.deviceLabel) return props.deviceLabel;
const coordinateFrame = publishedWorks
.map((session) => session.lab?.provenance.coordinate_frame)
.find((value) => typeof value === "string" && value.trim());
const sensorToken = typeof coordinateFrame === "string"
? coordinateFrame.split("-")[0]?.trim()
: "";
return sensorToken ? sensorToken.toLocaleUpperCase("ru-RU") : "Сенсорный риг";
}, [props.deviceLabel, publishedWorks]);
const sensorWorks = useMemo(() => {
const items: LaboratoryOption<LaboratoryWorkId>[] = [];
if (e28Model) {
items.push({
id: "e28-local-surface",
label: "LAB E28 · локальная поверхность L2.6",
});
}
if (
e29Result
&& sourceSessions.has(e29Result.linkedEvidence.sourceSessionId)
) {
items.push({
id: "e29-camera-geometry",
label: "LAB E29 · camera-first + geometry",
});
}
if (e30Result && sourceSessions.has(e30Result.sourceSessionId)) {
items.push({
id: "e30-evidence-review",
label: "LAB E30 · evidence review A2",
});
}
return items;
}, [e28Model, e29Result, e30Result, sourceSessions]);
const profiles = useMemo(() => {
const items: LaboratoryOption<LaboratoryProfileId>[] = [];
if (sensorWorks.length) {
items.push({
id: "sensor-fusion",
label: `${rigLabel} · камера + LiDAR · control plane`,
});
}
if (publishedWorks.length) {
items.push({
id: "published-perception",
label: `${rigLabel} · опубликованный perception pipeline`,
});
}
return items;
}, [publishedWorks.length, rigLabel, sensorWorks.length]);
const workOptions: readonly LaboratoryOption<LaboratoryWorkId>[] =
profileId === "sensor-fusion"
? sensorWorks
: publishedWorks.map((session) => ({
id: `session:${session.id}` as const,
label: `${session.lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`,
}));
const selectedSessionId = workId.startsWith("session:")
? workId.slice("session:".length)
: null;
const selectedSession = selectedSessionId
? publishedWorks.find((session) => session.id === selectedSessionId) ?? null
: null;
const e29SourceSession = e29Result
? sourceSessions.get(e29Result.linkedEvidence.sourceSessionId) ?? null
: null;
const e30SourceSession = e30Result
? sourceSessions.get(e30Result.sourceSessionId) ?? null
: null;
useEffect(() => {
if (
evidenceLoading
|| sessions.state === "idle"
|| sessions.state === "loading"
) return;
if (!profiles.some((profile) => profile.id === profileId)) {
const firstProfile = profiles[0];
if (!firstProfile) return;
setProfileId(firstProfile.id);
if (firstProfile.id === "sensor-fusion") {
const firstWork = sensorWorks[0];
if (firstWork) setWorkId(firstWork.id);
} else {
const first = publishedWorks[0];
if (first) setWorkId(`session:${first.id}`);
}
return;
}
if (!workOptions.some((work) => work.id === workId)) {
const firstWork = workOptions[0];
if (firstWork) setWorkId(firstWork.id);
}
}, [
evidenceLoading,
profileId,
profiles,
publishedWorks,
sensorWorks,
sessions.state,
workId,
workOptions,
]);
const selectProfile = (next: LaboratoryProfileId) => {
setProfileId(next);
if (next === "sensor-fusion") {
const first = sensorWorks[0];
if (first) setWorkId(first.id);
return;
}
const first = publishedWorks[0];
if (!first) return;
const nextWork = `session:${first.id}` as const;
setWorkId(nextWork);
void sessions.replay(first.id);
};
const selectWork = (next: LaboratoryWorkId) => {
setWorkId(next);
if (next.startsWith("session:")) {
void sessions.replay(next.slice("session:".length));
return;
}
if (next === "e29-camera-geometry" && e29SourceSession) {
void sessions.replay(e29SourceSession.id);
return;
}
if (next === "e30-evidence-review" && e30SourceSession) {
void sessions.replay(e30SourceSession.id);
}
};
if (
evidenceLoading
|| sessions.state === "idle"
|| sessions.state === "loading"
) {
return (
<div className="laboratory-result-pending" role="status">
<span className="busy-indicator" aria-hidden="true" />
<strong>Ревизия лабораторных данных</strong>
<p>Проверяем артефакты, связанные исходные записи и доступность replay.</p>
</div>
);
}
if (!profiles.length) {
return (
<div className="laboratory-result-pending" role="status">
<Icon name="database" size={20} />
<strong>Подтверждённых лабораторных работ нет</strong>
<p>
{evidenceError ?? sessions.error ?? "Непроверенные и отсутствующие результаты скрыты."}
</p>
</div>
);
}
const viewerFocused = Boolean(
props.observationLayout.focusedSourceId
|| props.observationLayout.maximizedFloatingSourceId,
);
return (
<div
className="lab-archive-workspace"
data-viewer-focused={viewerFocused ? "true" : undefined}
>
<LaboratorySelector
eyebrow="ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА"
title={profiles.find((profile) => profile.id === profileId)?.label ?? rigLabel}
description="Профиль фиксирует объект исследования, сенсорные модули и вычислительный контур. Исходные данные остаются read-only; профиль объединяет серию сопоставимых лабораторных работ."
label="Профиль"
value={profileId}
options={profiles}
onChange={selectProfile}
/>
<LaboratorySelector
eyebrow="ЛАБОРАТОРНАЯ РАБОТА"
title={workOptions.find((work) => work.id === workId)?.label ?? "Работа не выбрана"}
description="Выберите один зафиксированный эксперимент. Ниже откроются его задача и структурированный результат; viewer появляется только у опубликованного серверного доказательства."
label="Работа"
value={workId}
options={workOptions}
disabled={workOptions.length === 0}
onChange={selectWork}
/>
<div className="laboratory-work-output">
{workId === "e28-local-surface" ? (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E28 · локальная модель поверхности L2.6"
description="Запись RAVNOVES00 воспроизводится через bounded shadow-контур. Модель оценивает поверхность, препятствия, временные скачки и ошибку предсказания без изменения источника и без командного канала."
status="Проверенные артефакты"
statusTone="success"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · LiDAR + pose · worker D` },
{
label: "Покрытие",
value: `${formatNumber(e28Model?.metrics.frames.valid ?? 0, 0)} / ${formatNumber(e28Model?.metrics.frames.total ?? 0, 0)} кадров`,
},
{ label: "Режим", value: "Recorded-source-paced shadow" },
{ label: "Контур", value: "Read-only · hash verified" },
]}
method={{
completeness: "complete",
executionClass: e28Model?.method.executionClass ?? "deterministic",
pipelineId: e28Model?.method.pipelineId ?? "local-surface/unavailable",
components: [
{
kind: "source",
name: e28Model?.sourcePackId ?? "Источник не загружен",
version: "immutable vendor MAP + pose",
role: "read-only LiDAR evidence",
identitySha256: digestFromContentId(e28Model?.sourcePackId),
},
{
kind: "algorithm",
name: e28Model?.method.algorithm ?? "Rolling local surface",
version: e28Model?.method.pipelineId ?? "—",
role: "robust local plane, occupancy and temporal residuals",
identitySha256: e28Model?.method.producerSha256 ?? null,
},
{
kind: "runtime",
name: "Mission Core worker D",
version: "recorded-source-paced shadow",
role: "bounded passive replay",
identitySha256: null,
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
title="Диагностическая поверхность и кадры LAB E28"
kind="diagnostic-model"
>
<LidarQualityWorkspace
embedded
deviceLabel={props.deviceLabel}
onOpenObservation={() => props.navigation.openView("spatial-scene")}
/>
</LaboratoryEvidence>
)}
/>
) : workId === "e29-camera-geometry" && e29Result && e29SourceSession ? (
<E29LaboratoryResult
props={props}
rigLabel={rigLabel}
result={e29Result}
sourceSession={e29SourceSession}
loading={sessions.replayingSessionId === e29SourceSession.id}
error={
sessions.failedSessionId === e29SourceSession.id
? sessions.error
: null
}
/>
) : workId === "e30-evidence-review" && e30Result && e30SourceSession ? (
<E30LaboratoryResult
rigLabel={rigLabel}
result={e30Result}
sourceSession={e30SourceSession}
/>
) : selectedSession ? (
<PublishedLaboratoryResult
props={props}
session={selectedSession}
loading={sessions.replayingSessionId === selectedSession.id}
error={sessions.failedSessionId === selectedSession.id ? sessions.error : null}
/>
) : (
<div className="laboratory-result-pending">
<Icon name="database" size={20} />
<strong>Работа не прошла ревизию</strong>
<p>Неподтверждённый результат скрыт из лабораторного каталога.</p>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,121 @@
import assert from "node:assert/strict";
import { readdir, readFile } from "node:fs/promises";
import { extname, join } from "node:path";
import { test } from "node:test";
const sourceRoot = new URL("../src/", import.meta.url);
async function sourceFiles(relativeDirectory) {
const directory = new URL(`${relativeDirectory}/`, sourceRoot);
const entries = await readdir(directory, {
recursive: true,
withFileTypes: true,
});
return entries
.filter((entry) => entry.isFile() && [".ts", ".tsx"].includes(extname(entry.name)))
.map((entry) => join(entry.parentPath, entry.name));
}
async function read(relativePath) {
return readFile(new URL(relativePath, sourceRoot), "utf8");
}
test("dependency direction keeps core and reusable components below workspaces", async () => {
const coreFiles = await sourceFiles("core");
const componentFiles = await sourceFiles("components");
for (const file of coreFiles) {
const source = await readFile(file, "utf8");
assert.doesNotMatch(
source,
/from\s+["'][^"']*(?:workspaces|\/App)["']/,
`${file} imports an application composition layer`,
);
assert.doesNotMatch(
source,
/@nodedc\/ui-(?:react|dom)/,
`${file} imports a visual adapter from the domain layer`,
);
}
for (const file of componentFiles) {
const source = await readFile(file, "utf8");
assert.doesNotMatch(
source,
/from\s+["'][^"']*(?:workspaces|\/App)["']/,
`${file} imports a workspace composition layer`,
);
}
});
test("shared visual primitives come only from the Design Guideline packages", async () => {
const packageJson = JSON.parse(
await readFile(new URL("../package.json", import.meta.url), "utf8"),
);
for (const packageName of [
"@nodedc/tokens",
"@nodedc/ui-core",
"@nodedc/ui-react",
]) {
assert.equal(typeof packageJson.dependencies[packageName], "string");
}
const files = await sourceFiles(".");
for (const file of files) {
const source = await readFile(file, "utf8");
assert.doesNotMatch(source, /lucide-react/);
assert.doesNotMatch(source, /NODEDC_DESIGN_GUIDELINE\/(?:apps|src)\//);
}
});
test("laboratory UI is a bounded feature slice, not a central workspace branch", async () => {
const workspaceHub = await read("workspaces/Workspaces.tsx");
const laboratory = await read(
"workspaces/laboratory/LaboratoryArchiveWorkspace.tsx",
);
const workspaceCss = await read("styles/workspaces.css");
const laboratoryCss = await read("styles/laboratory.css");
const e30Review = await read("workspaces/E30ReviewWorkspace.tsx");
const e30HumanReview = await read(
"workspaces/E30HumanReviewPanel.tsx",
);
const e30HumanReviewCss = await read("styles/e30-human-review.css");
assert.doesNotMatch(
workspaceHub,
/fetchE(?:29|30)|LaboratoryWorkTemplate|function\s+E(?:29|30)LaboratoryResult/,
);
assert.match(
workspaceHub,
/<LaboratoryArchiveWorkspace[\s\S]*SpatialView=\{SpatialWorkspace\}/,
);
assert.match(laboratory, /export function LaboratoryArchiveWorkspace/);
assert.match(laboratory, /type LaboratoryWorkspaceProps = WorkspaceRendererProps/);
assert.doesNotMatch(workspaceCss, /\.(?:lab-|laboratory-|e30-)/);
assert.match(laboratoryCss, /\.laboratory-work-template/);
assert.match(laboratoryCss, /\.e30-review-workspace/);
assert.doesNotMatch(laboratory, /E30HumanReviewPanel/);
assert.match(e30Review, /<E30HumanReviewPanel/);
assert.doesNotMatch(e30Review, /E30EngineeringAuditPanel/);
assert.match(e30HumanReview, /from "@nodedc\/ui-react"/);
assert.doesNotMatch(laboratoryCss, /\.e30-human-review/);
assert.match(e30HumanReviewCss, /\.e30-human-review/);
});
test("central composition files cannot silently become monoliths again", async () => {
const ratchets = [
["App.tsx", 1_250],
["workspaces/Workspaces.tsx", 1_200],
["workspaces/laboratory/LaboratoryArchiveWorkspace.tsx", 1_000],
["styles/workspaces.css", 4_350],
["styles/laboratory.css", 900],
];
for (const [relativePath, maximumLines] of ratchets) {
const lineCount = (await read(relativePath)).split("\n").length;
assert.ok(
lineCount <= maximumLines,
`${relativePath} has ${lineCount} lines; split the feature instead of raising ${maximumLines}`,
);
}
});
@@ -0,0 +1,194 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let parseE30EngineeringCatalog;
let parseE30EngineeringDecision;
let fetchE30EngineeringDecision;
let E30EngineeringContractError;
const resultId = `e30-materialization-${"a".repeat(64)}`;
const generationId = `e30-engineering-generation-${"b".repeat(64)}`;
const itemId = `e30-review-item-${"c".repeat(64)}`;
function generation(overrides = {}) {
return {
generation_id: generationId,
created_at_utc: "2026-07-26T22:01:18.464Z",
materialization_id: resultId,
producer: {
kind: "ai-assisted-engineering-review",
producer_id: "codex:a3-engineering-review",
method_id: "camera-lidar-42-sheet-audit/v1",
review_sheet_identity_sha256: "d".repeat(64),
claims_human_ground_truth: false,
},
summary: {
item_count: 486,
reviewed_item_count: 486,
verdict_distribution: {
confirmed: 401,
corrected: 80,
"insufficient-evidence": 5,
},
detector_distribution: { valid: 259, "false-positive": 78 },
projection_distribution: { aligned: 371, "not-assessable": 115 },
point_ownership_distribution: { object: 108 },
human_exception_count: 5,
mean_confidence: 0.8705,
},
cause_distribution: {
schema_version: "missioncore.e30-engineering-causes/v1",
item_count_with_cause: 312,
reasons: [
{ reason_code: "detector_error", count: 109 },
{ reason_code: "time_mismatch", count: 91 },
],
},
human_exceptions: [{
item_id: itemId,
review_key: "semantic:162:7",
source_stratum: "camera-only",
confidence: 0.48,
review_prompt: {
question: "Белый кластер — самостоятельное препятствие?",
focus: "Проверьте выбранные белые точки.",
effects: {
"object-present": "Сохранить занятую геометрию.",
"background-or-noise": "Исключить кластер.",
"insufficient-evidence": "Оставить неизвестным.",
},
},
}],
ai_review_complete: true,
human_exception_complete: false,
human_review_complete: false,
lab_published: false,
access: "read-only",
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
},
...overrides,
};
}
function catalog(overrides = {}) {
return {
schema_version: "missioncore.laboratory-e30-engineering-generations/v1",
configured: true,
items: [generation()],
candidate_total: 1,
invalid_total: 0,
access: "read-only",
...overrides,
};
}
function decision(overrides = {}) {
return {
schema_version: "missioncore.laboratory-e30-engineering-decision/v1",
generation_id: generationId,
decision: {
sequence: 362,
item_id: itemId,
review_key: "semantic:343:2",
source_stratum: "conflict",
verdict: "corrected",
effective_stratum: null,
detector_assessment: "false-positive",
projection_assessment: "aligned",
point_ownership: "surface-or-background",
cause_code: "detector_error",
confidence: 0.95,
human_exception_required: false,
exception_reason: null,
evidence_note: "Кадр 343. Рамка находится на ограждении.",
review_sheet: {
path: "conflict-01.jpg",
sha256: "e".repeat(64),
ordinal: 1,
},
...overrides,
},
access: "read-only",
};
}
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
parseE30EngineeringCatalog,
parseE30EngineeringDecision,
fetchE30EngineeringDecision,
E30EngineeringContractError,
} = await server.ssrLoadModule(
"/src/core/laboratory/e30Engineering.ts",
));
});
after(async () => {
await server?.close();
});
test("decodes immutable A3 summary and per-item engineering decision", () => {
const decodedCatalog = parseE30EngineeringCatalog(catalog());
const decodedDecision = parseE30EngineeringDecision(decision());
assert.equal(decodedCatalog.items[0].summary.reviewedItemCount, 486);
assert.equal(decodedCatalog.items[0].summary.humanExceptionCount, 5);
assert.equal(decodedCatalog.items[0].humanExceptions[0].reviewKey, "semantic:162:7");
assert.equal(
decodedCatalog.items[0].humanExceptions[0].reviewPrompt.effects["object-present"],
"Сохранить занятую геометрию.",
);
assert.equal(decodedDecision.verdict, "corrected");
assert.equal(decodedDecision.effectiveStratum, null);
assert.equal(decodedDecision.detectorAssessment, "false-positive");
});
test("rejects a generation that claims human ground truth", () => {
assert.throws(
() => parseE30EngineeringCatalog(catalog({
items: [generation({
producer: {
...generation().producer,
claims_human_ground_truth: true,
},
})],
})),
E30EngineeringContractError,
);
});
test("fetches one content-addressed item decision", async () => {
const requests = [];
const decoded = await fetchE30EngineeringDecision(
resultId,
generationId,
itemId,
{
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
return new Response(JSON.stringify(decision()), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
},
);
assert.equal(decoded.pointOwnership, "surface-or-background");
assert.deepEqual(requests, [{
input:
`/api/v1/laboratory/e30/reviews/${resultId}/engineering-generations/`
+ `${generationId}/items/${itemId}`,
method: "GET",
}]);
});
@@ -0,0 +1,207 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let parseE30HumanReviewDraft;
let parseE30HumanReviewFinalized;
let createOrResumeE30HumanReview;
let saveE30HumanReviewDecision;
let finalizeE30HumanReview;
let E30ReviewContractError;
const resultId = `e30-materialization-${"a".repeat(64)}`;
const engineeringGenerationId =
`e30-engineering-generation-${"b".repeat(64)}`;
const draftId = `e30-human-draft-${"c".repeat(64)}`;
const reviewGenerationId = `e30-review-generation-${"d".repeat(64)}`;
const firstItemId = `e30-review-item-${"e".repeat(64)}`;
const secondItemId = `e30-review-item-${"f".repeat(64)}`;
const firstEventId = `e30-review-event-${"1".repeat(64)}`;
const secondEventId = `e30-review-event-${"2".repeat(64)}`;
function decision(overrides = {}) {
return {
item_id: firstItemId,
source_stratum: "conflict",
disposition: "object-present",
notes: null,
event_id: firstEventId,
decided_at_utc: "2026-07-27T06:00:00Z",
...overrides,
};
}
function draft(overrides = {}) {
return {
schema_version: "missioncore.e30-human-review-draft/v2",
draft_id: draftId,
materialization_id: resultId,
engineering_generation_id: engineeringGenerationId,
reviewer_id: "DC",
created_at_utc: "2026-07-27T06:00:00Z",
state: "active",
revision: 1,
item_count: 2,
reviewed_item_count: 1,
remaining_item_count: 1,
disposition_distribution: { "object-present": 1 },
generation_id: null,
decisions: [decision()],
lab_published: false,
access: "review-write",
...overrides,
};
}
function finalized() {
return {
schema_version: "missioncore.laboratory-e30-human-review-finalized/v2",
draft: draft({
state: "finalized",
revision: 2,
reviewed_item_count: 2,
remaining_item_count: 0,
disposition_distribution: {
"object-present": 1,
"background-or-noise": 1,
},
generation_id: reviewGenerationId,
decisions: [
decision(),
decision({
item_id: secondItemId,
source_stratum: "unknown",
disposition: "background-or-noise",
event_id: secondEventId,
}),
],
access: "read-only",
}),
generation: {
schema_version: "missioncore.e30-human-review-generation/v2",
generation_id: reviewGenerationId,
materialization_id: resultId,
engineering_generation_id: engineeringGenerationId,
reviewer_id: "DC",
created_at_utc: "2026-07-27T06:05:00Z",
item_count: 2,
disposition_distribution: {
"object-present": 1,
"background-or-noise": 1,
},
coverage: {
expected_item_count: 2,
reviewed_item_count: 2,
complete: true,
},
human_review_complete: true,
lab_published: false,
access: "read-only",
},
};
}
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
parseE30HumanReviewDraft,
parseE30HumanReviewFinalized,
createOrResumeE30HumanReview,
saveE30HumanReviewDecision,
finalizeE30HumanReview,
} = await server.ssrLoadModule(
"/src/core/laboratory/e30HumanReview.ts",
));
({ E30ReviewContractError } = await server.ssrLoadModule(
"/src/core/laboratory/e30Review.ts",
));
});
after(async () => {
await server?.close();
});
test("decodes an exception-only draft and immutable final generation", () => {
const active = parseE30HumanReviewDraft(draft());
const complete = parseE30HumanReviewFinalized(finalized());
assert.equal(active.engineeringGenerationId, engineeringGenerationId);
assert.equal(active.reviewedItemCount, 1);
assert.equal(active.decisions[0].disposition, "object-present");
assert.equal(complete.draft.state, "finalized");
assert.equal(complete.generation.humanReviewComplete, true);
assert.equal(complete.generation.labPublished, false);
});
test("rejects unknown dispositions and finalized writable access", () => {
assert.throws(
() => parseE30HumanReviewDraft(draft({
decisions: [decision({ disposition: "invented" })],
})),
E30ReviewContractError,
);
assert.throws(
() => parseE30HumanReviewDraft(draft({
state: "finalized",
access: "review-write",
})),
E30ReviewContractError,
);
});
test("uses generation-bound write endpoints and explicit finalization", async () => {
const requests = [];
const fetcher = async (input, init) => {
const body = init?.body ? JSON.parse(String(init.body)) : null;
requests.push({ input: String(input), method: init?.method, body });
const payload = String(input).includes("/finalize?")
? finalized()
: draft();
return new Response(JSON.stringify(payload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
await createOrResumeE30HumanReview(
resultId,
engineeringGenerationId,
"DC",
{ fetcher },
);
await saveE30HumanReviewDecision(
resultId,
engineeringGenerationId,
draftId,
firstItemId,
{
expectedRevision: 1,
idempotencyKey: "ui-decision-001",
disposition: "insufficient-evidence",
notes: null,
},
{ fetcher },
);
await finalizeE30HumanReview(
resultId,
engineeringGenerationId,
draftId,
2,
{ fetcher },
);
assert.equal(requests[0].method, "POST");
assert.deepEqual(requests[0].body, {
reviewer_id: "DC",
engineering_generation_id: engineeringGenerationId,
});
assert.match(requests[1].input, /engineering_generation_id=/);
assert.equal(requests[1].body.disposition, "insufficient-evidence");
assert.equal(requests[2].body.confirm_generation, true);
});
@@ -0,0 +1,244 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let parseE30ReviewCatalog;
let parseE30ReviewItems;
let parseE30ReviewItemDetail;
let fetchE30ReviewItems;
let E30ReviewContractError;
const resultId = `e30-materialization-${"a".repeat(64)}`;
const reviewPackId = `e30-review-pack-${"b".repeat(64)}`;
const e29ResultId = `e29-camera-geometry-${"c".repeat(64)}`;
const itemId = `e30-review-item-${"d".repeat(64)}`;
function result(overrides = {}) {
return {
result_id: resultId,
created_at_utc: "2026-07-26T00:00:00Z",
review_pack_id: reviewPackId,
e29_result_id: e29ResultId,
source_session_id: "20260720T065719Z_viewer_live",
item_count: 486,
stratum_counts: {
conflict: 38,
agree: 96,
"camera-only": 128,
unknown: 96,
"geometry-only": 128,
},
reason_taxonomy: ["time_mismatch", "geometry_mismatch", "other"],
projection: {
width: 800,
height: 600,
source_id: "sensor.camera.right",
calibration_slot: "camera_1",
},
camera_evidence_available: true,
human_review_complete: false,
lab_published: false,
access: "read-only",
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
},
...overrides,
};
}
function item(overrides = {}) {
return {
item_id: itemId,
sequence: 212,
review_key: "semantic:1800:1",
stratum: "conflict",
range_bucket: "unavailable",
frame_index: 1800,
source_frame_index: 1800,
session_seconds: 215.323857292,
locator: {
kind: "semantic-observation",
observation_index: 1,
},
snapshot: {
label: "car",
geometry_status: "conflict",
geometry_reason: "camera-object-region-observed-as-local-surface",
range_m: null,
bbox_xyxy: [223.6, 270.2, 282.4, 305.2],
},
materialization: {
frame_point_count: 1813,
projected_point_count: 3,
candidate_point_count: 2,
selected_point_count: 1,
rejected_candidate_point_count: 1,
selected_and_candidate_lossless: true,
free_space_valid: false,
human_review_complete: false,
detector_score: 0.83,
},
camera_frame_available: true,
engineering_triage: {
provenance: "deterministic-evidence-readiness/v1",
state: "ready-for-ai-review",
attention: "standard",
signals: [],
semantic_verdict: null,
human_exception_required: null,
},
review: {
state: "unreviewed",
reason_code: null,
notes: null,
},
access: "read-only",
...overrides,
};
}
function catalog(overrides = {}) {
return {
schema_version: "missioncore.laboratory-e30-catalog/v1",
configured: true,
items: [result()],
candidate_total: 1,
invalid_total: 0,
access: "read-only",
...overrides,
};
}
function items(overrides = {}) {
return {
schema_version: "missioncore.laboratory-e30-items/v1",
result_id: resultId,
stratum: "conflict",
items: [item()],
total: 38,
next_cursor: null,
reason_taxonomy: ["time_mismatch", "geometry_mismatch", "other"],
access: "read-only",
...overrides,
};
}
function detail(overrides = {}) {
return {
schema_version: "missioncore.laboratory-e30-item-detail/v1",
result_id: resultId,
item: {
...item(),
camera_frame: {
available: true,
url: `/api/v1/laboratory/e30/reviews/${resultId}/items/${itemId}/camera-frame?generation=${"e".repeat(64)}`,
sha256: "e".repeat(64),
width: 800,
height: 600,
source_frame_index: 1800,
exact_source_frame: true,
},
selected: {
source_indices: [7],
points_map_xyz_m: [[1, 2, 3]],
},
candidate: {
source_indices: [7, 8],
points_map_xyz_m: [[1, 2, 3], [1.1, 2.1, 3.1]],
},
projection: {
source_indices: [7, 8, 9],
points_map_xyz_m: [[1, 2, 3], [1.1, 2.1, 3.1], [2, 3, 4]],
pixels_xy: [[220, 270], [230, 275], [500, 400]],
depth_m: [3, 3.1, 4],
point_class: [2, 1, 0],
point_height_m: [0.8, 0.1, 0],
candidate_mask: [1, 1, 0],
selected_mask: [1, 0, 0],
},
pose: {
position_map_xyz_m: [10, 11, 0],
orientation_map_from_lidar_xyzw: [0, 0, 0, 1],
},
...overrides,
},
access: "read-only",
};
}
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
parseE30ReviewCatalog,
parseE30ReviewItems,
parseE30ReviewItemDetail,
fetchE30ReviewItems,
E30ReviewContractError,
} = await server.ssrLoadModule("/src/core/laboratory/e30Review.ts"));
});
after(async () => {
await server?.close();
});
test("decodes immutable E30 catalog, strata and exact point evidence", () => {
const decodedCatalog = parseE30ReviewCatalog(catalog());
const decodedItems = parseE30ReviewItems(items());
const decodedDetail = parseE30ReviewItemDetail(detail());
assert.equal(decodedCatalog.items[0].stratumCounts.conflict, 38);
assert.equal(decodedCatalog.items[0].humanReviewComplete, false);
assert.equal(decodedItems.items[0].sessionSeconds, 215.323857292);
assert.deepEqual(decodedDetail.selected.sourceIndices, [7]);
assert.deepEqual(decodedDetail.projection.selectedMask, [1, 0, 0]);
assert.equal(decodedDetail.cameraFrame.exactSourceFrame, true);
});
test("rejects authority escalation and inconsistent materialized arrays", () => {
assert.throws(
() => parseE30ReviewCatalog(catalog({
items: [result({
authority: {
commands_enabled: true,
navigation_or_safety_accepted: false,
},
})],
})),
E30ReviewContractError,
);
assert.throws(
() => parseE30ReviewItemDetail(detail({
selected: {
source_indices: [7, 8],
points_map_xyz_m: [[1, 2, 3]],
},
})),
E30ReviewContractError,
);
});
test("fetches the canonical read-only stratum endpoint", async () => {
const requests = [];
const decoded = await fetchE30ReviewItems(resultId, "conflict", {
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
return new Response(JSON.stringify(items()), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
});
assert.equal(decoded.total, 38);
assert.deepEqual(requests, [{
input: `/api/v1/laboratory/e30/reviews/${resultId}/items?stratum=conflict&limit=128&cursor=0`,
method: "GET",
}]);
});
@@ -0,0 +1,80 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
const workspaceSourceUrl = new URL(
"../src/workspaces/E30ReviewWorkspace.tsx",
import.meta.url,
);
const presentationSourceUrl = new URL(
"../src/components/laboratory/LaboratoryPresentation.tsx",
import.meta.url,
);
const viewerSourceUrl = new URL(
"../src/components/laboratory/LaboratoryEvidenceViewer.tsx",
import.meta.url,
);
const laboratoryStylesUrl = new URL(
"../src/styles/laboratory.css",
import.meta.url,
);
test("E30 uses one reusable evidence viewer with camera, 3D and expand controls", async () => {
const [workspaceSource, viewerSource, laboratoryStyles] = await Promise.all([
readFile(workspaceSourceUrl, "utf8"),
readFile(viewerSourceUrl, "utf8"),
readFile(laboratoryStylesUrl, "utf8"),
]);
assert.match(workspaceSource, /<LaboratoryEvidenceViewer/);
assert.match(workspaceSource, /\{ value: "3d", label: "3D" \}/);
assert.match(workspaceSource, /\{ value: "camera", label: "Камера" \}/);
assert.match(workspaceSource, /<E30EvidenceProjection/);
assert.match(workspaceSource, /<E30EvidenceTelemetry/);
assert.match(workspaceSource, /aria-pressed=\{pointLayerVisible\}/);
assert.match(workspaceSource, />\s*LiDAR\s*<\/Button>/);
assert.match(workspaceSource, /review: "Проверка"/);
assert.doesNotMatch(workspaceSource, /e30-review-evidence__facts/);
assert.doesNotMatch(workspaceSource, /E30EngineeringAuditPanel/);
assert.match(viewerSource, /createPortal/);
assert.match(viewerSource, /name=\{expanded \? "minimize" : "expand"\}/);
assert.match(
laboratoryStyles,
/\.e30-evidence-telemetry dl\s*\{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)/s,
);
assert.match(
laboratoryStyles,
/\.e30-projection-scene canvas\s*\{[^}]*object-fit:\s*cover/s,
);
});
test("E30 exception review is a separate block after the fixed evidence workspace", async () => {
const workspaceSource = await readFile(workspaceSourceUrl, "utf8");
const bodyStart = workspaceSource.indexOf(
'<div className="e30-review-workspace__body">',
);
const detailClose = workspaceSource.indexOf(
"</div>\n\n {!detailLoading",
bodyStart,
);
const reviewPanel = workspaceSource.indexOf(
"<E30HumanReviewPanel",
bodyStart,
);
assert.ok(bodyStart >= 0);
assert.ok(detailClose > bodyStart);
assert.ok(reviewPanel > detailClose);
});
test("LAB product surface has a compact canonical summary and no roadmap footer", async () => {
const [workspaceSource, presentationSource] = await Promise.all([
readFile(workspaceSourceUrl, "utf8"),
readFile(presentationSourceUrl, "utf8"),
]);
assert.match(presentationSource, /export function LaboratorySummary/);
assert.match(presentationSource, /export function LaboratoryWorkTemplate/);
assert.doesNotMatch(workspaceSource, /СЛЕДУЮЩИЙ GATE/);
assert.doesNotMatch(workspaceSource, /e30-review-workspace__taxonomy/);
});
@@ -178,23 +178,30 @@ test("data recordings keep the compact session dropdown and laboratory results s
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
"utf8",
);
const laboratorySource = await readFile(
new URL(
"../src/workspaces/laboratory/LaboratoryArchiveWorkspace.tsx",
import.meta.url,
),
"utf8",
);
const productSource = await readFile(
new URL("../src/productModel.ts", import.meta.url),
"utf8",
);
const recordingsSource = workspaceSource.slice(
workspaceSource.indexOf("function RecordingsWorkspace"),
workspaceSource.indexOf("type LaboratoryProfileId"),
workspaceSource.indexOf("export function WorkspaceRenderer"),
);
assert.match(appSource, /activeDefinition\.kind === "recordings"[\s\S]*<ObservationSessionSelect/);
assert.match(recordingsSource, /<SpatialWorkspace \{\.\.\.props\} \/>/);
assert.doesNotMatch(recordingsSource, /ObservationSessionArchive/);
assert.match(productSource, /label: "Лабораторные контуры"/);
assert.match(workspaceSource, /ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА/);
assert.match(workspaceSource, /ЛАБОРАТОРНАЯ РАБОТА/);
assert.match(workspaceSource, /e28-local-surface/);
assert.match(workspaceSource, /e29-camera-geometry/);
assert.match(laboratorySource, /ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА/);
assert.match(laboratorySource, /ЛАБОРАТОРНАЯ РАБОТА/);
assert.match(laboratorySource, /e28-local-surface/);
assert.match(laboratorySource, /e29-camera-geometry/);
});
test("source and laboratory catalogs are requested as disjoint backend projections", async () => {
@@ -31,11 +31,21 @@ test("top navigation has no Center and Park owns contour health first", () => {
test("every laboratory result uses the shared evidence template", async () => {
const source = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
new URL(
"../src/workspaces/laboratory/LaboratoryArchiveWorkspace.tsx",
import.meta.url,
),
"utf8",
);
const presentation = await readFile(
new URL(
"../src/components/laboratory/LaboratoryPresentation.tsx",
import.meta.url,
),
"utf8",
);
const css = await readFile(
new URL("../src/styles/workspaces.css", import.meta.url),
new URL("../src/styles/laboratory.css", import.meta.url),
"utf8",
);
const app = await readFile(
@@ -43,11 +53,14 @@ test("every laboratory result uses the shared evidence template", async () => {
"utf8",
);
assert.match(source, /function LaboratoryWorkTemplate\(/);
assert.match(source, /function LaboratoryEvidence\(/);
assert.match(source, /function LaboratoryMethodCard\(/);
assert.match(source, /method:\s*ReactNode/);
assert.match(source, /data-evidence-kind=\{kind\}/);
assert.match(source, /LaboratoryWorkTemplate,/);
assert.match(source, /LaboratorySummary,/);
assert.match(presentation, /export function LaboratoryWorkTemplate\(/);
assert.match(presentation, /export function LaboratoryEvidence\(/);
assert.match(presentation, /export function LaboratorySummary\(/);
assert.match(presentation, /summary:\s*ReactNode/);
assert.match(presentation, /data-evidence-kind=\{kind\}/);
assert.doesNotMatch(source, /function LaboratoryWorkTemplate\(/);
assert.match(source, /data-viewer-focused=/);
assert.match(css, /height:\s*clamp\(42rem,\s*68vh,\s*58rem\)/);
assert.match(css, /resize:\s*vertical/);