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>
);
}