feat(lab): canonize versioned report template
This commit is contained in:
@@ -31,6 +31,28 @@ export interface LaboratoryMethod {
|
||||
components: readonly LaboratoryMethodComponent[];
|
||||
}
|
||||
|
||||
export interface LaboratoryBrief {
|
||||
question: string;
|
||||
approach: string;
|
||||
principalResult: string;
|
||||
limitation: string;
|
||||
}
|
||||
|
||||
export interface LaboratoryResultMetric {
|
||||
label: string;
|
||||
value: string;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
export interface LaboratoryResultConclusion {
|
||||
proved: string;
|
||||
notProved: string;
|
||||
decision: string;
|
||||
}
|
||||
|
||||
export const LABORATORY_REPORT_TEMPLATE_VERSION =
|
||||
"missioncore.laboratory-report/v1";
|
||||
|
||||
const EXECUTION_LABELS: Record<LaboratoryExecutionClass, string> = {
|
||||
deterministic: "Детерминированный",
|
||||
"ai-inference": "AI inference",
|
||||
@@ -96,6 +118,7 @@ export function LaboratorySummary({
|
||||
status,
|
||||
statusTone = "neutral",
|
||||
facts,
|
||||
brief,
|
||||
method,
|
||||
}: {
|
||||
title: string;
|
||||
@@ -103,6 +126,7 @@ export function LaboratorySummary({
|
||||
status: string;
|
||||
statusTone?: "neutral" | "success" | "warning" | "danger" | "accent";
|
||||
facts: readonly { label: string; value: string }[];
|
||||
brief: LaboratoryBrief;
|
||||
method?: LaboratoryMethod | null;
|
||||
}) {
|
||||
const methodComplete = method?.completeness === "complete";
|
||||
@@ -126,6 +150,25 @@ export function LaboratorySummary({
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<dl className="laboratory-summary__brief">
|
||||
<div>
|
||||
<dt>Задача</dt>
|
||||
<dd>{brief.question}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Как проверяли</dt>
|
||||
<dd>{brief.approach}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Главный результат</dt>
|
||||
<dd>{brief.principalResult}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Ограничение</dt>
|
||||
<dd>{brief.limitation}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{method ? (
|
||||
<div className="laboratory-summary__method">
|
||||
<header>
|
||||
@@ -193,6 +236,69 @@ export function LaboratoryEvidence({
|
||||
);
|
||||
}
|
||||
|
||||
export function LaboratoryConclusion({
|
||||
proved,
|
||||
notProved,
|
||||
decision,
|
||||
}: {
|
||||
proved: string;
|
||||
notProved: string;
|
||||
decision: string;
|
||||
}) {
|
||||
return (
|
||||
<dl className="laboratory-result-conclusion">
|
||||
<div>
|
||||
<dt>Доказано</dt>
|
||||
<dd>{proved}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Не доказано</dt>
|
||||
<dd>{notProved}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Решение</dt>
|
||||
<dd>{decision}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
export function LaboratoryResultSummary({
|
||||
title,
|
||||
status,
|
||||
statusTone = "neutral",
|
||||
metrics,
|
||||
conclusion,
|
||||
}: {
|
||||
title: string;
|
||||
status: string;
|
||||
statusTone?: "neutral" | "success" | "warning" | "danger" | "accent";
|
||||
metrics: readonly LaboratoryResultMetric[];
|
||||
conclusion: LaboratoryResultConclusion;
|
||||
}) {
|
||||
return (
|
||||
<section className="laboratory-result-summary">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||
<h2>{title}</h2>
|
||||
</div>
|
||||
<StatusBadge tone={statusTone}>{status}</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
{metrics.map((metric) => (
|
||||
<div key={metric.label}>
|
||||
<span>{metric.label}</span>
|
||||
<strong>{metric.value}</strong>
|
||||
<small>{metric.hint}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<LaboratoryConclusion {...conclusion} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function LaboratoryWorkTemplate({
|
||||
summary,
|
||||
evidence,
|
||||
@@ -205,7 +311,10 @@ export function LaboratoryWorkTemplate({
|
||||
details?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="laboratory-work-template">
|
||||
<div
|
||||
className="laboratory-work-template"
|
||||
data-template-contract={LABORATORY_REPORT_TEMPLATE_VERSION}
|
||||
>
|
||||
{summary}
|
||||
{evidence}
|
||||
{result}
|
||||
|
||||
@@ -64,15 +64,22 @@ export interface E33LaboratoryResult {
|
||||
};
|
||||
metrics: {
|
||||
sourceFrames: number;
|
||||
sourceAvailableFrames: number;
|
||||
sourceUnavailableFrames: number;
|
||||
deliveredFrames: number;
|
||||
inputSuperseded: number;
|
||||
resultSuperseded: number;
|
||||
effectiveDeliveryFps: number;
|
||||
sourceSpanSeconds: number;
|
||||
deadlineMissFraction: number;
|
||||
processingP95Ms: number;
|
||||
releaseLagP95Ms: number;
|
||||
resultAgeP95Ms: number;
|
||||
resultAgeMaxMs: number;
|
||||
resultDeadlineMs: number;
|
||||
processRssP95Mib: number;
|
||||
processRssMaxMib: number;
|
||||
processRssLimitMib: number;
|
||||
gpuUtilizationP95Percent: number;
|
||||
gpuVisible: boolean;
|
||||
workQueueCapacity: number;
|
||||
@@ -314,15 +321,22 @@ function parseE33(value: unknown): E33LaboratoryResult {
|
||||
},
|
||||
metrics: {
|
||||
sourceFrames: integerValue(metrics.source_frames, "E33.metrics.source_frames"),
|
||||
sourceAvailableFrames: integerValue(metrics.source_available_frames, "E33.metrics.source_available_frames"),
|
||||
sourceUnavailableFrames: integerValue(metrics.source_unavailable_frames, "E33.metrics.source_unavailable_frames"),
|
||||
deliveredFrames: integerValue(metrics.delivered_frames, "E33.metrics.delivered_frames"),
|
||||
inputSuperseded: integerValue(metrics.input_superseded, "E33.metrics.input_superseded"),
|
||||
resultSuperseded: integerValue(metrics.result_superseded, "E33.metrics.result_superseded"),
|
||||
effectiveDeliveryFps: numberValue(metrics.effective_delivery_fps, "E33.metrics.effective_delivery_fps"),
|
||||
sourceSpanSeconds: numberValue(metrics.source_span_seconds, "E33.metrics.source_span_seconds"),
|
||||
deadlineMissFraction: numberValue(metrics.deadline_miss_fraction, "E33.metrics.deadline_miss_fraction"),
|
||||
processingP95Ms: numberValue(metrics.processing_p95_ms, "E33.metrics.processing_p95_ms"),
|
||||
releaseLagP95Ms: numberValue(metrics.release_lag_p95_ms, "E33.metrics.release_lag_p95_ms"),
|
||||
resultAgeP95Ms: numberValue(metrics.result_age_p95_ms, "E33.metrics.result_age_p95_ms"),
|
||||
resultAgeMaxMs: numberValue(metrics.result_age_max_ms, "E33.metrics.result_age_max_ms"),
|
||||
resultDeadlineMs: numberValue(metrics.result_deadline_ms, "E33.metrics.result_deadline_ms"),
|
||||
processRssP95Mib: numberValue(metrics.process_rss_p95_mib, "E33.metrics.process_rss_p95_mib"),
|
||||
processRssMaxMib: numberValue(metrics.process_rss_max_mib, "E33.metrics.process_rss_max_mib"),
|
||||
processRssLimitMib: numberValue(metrics.process_rss_limit_mib, "E33.metrics.process_rss_limit_mib"),
|
||||
gpuUtilizationP95Percent: numberValue(metrics.gpu_utilization_p95_percent, "E33.metrics.gpu_utilization_p95_percent"),
|
||||
gpuVisible: booleanValue(metrics.gpu_visible, "E33.metrics.gpu_visible"),
|
||||
workQueueCapacity: integerValue(metrics.work_queue_capacity, "E33.metrics.work_queue_capacity"),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import "./styles/shell.css";
|
||||
@import "./styles/workspaces.css";
|
||||
@import "./styles/laboratory.css";
|
||||
@import "./styles/laboratory-reporting.css";
|
||||
@import "./styles/e30-human-review.css";
|
||||
@import "./styles/spatial.css";
|
||||
@import "./styles/device.css";
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
.laboratory-summary__brief {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.65rem;
|
||||
}
|
||||
|
||||
.laboratory-summary__brief > div {
|
||||
display: grid;
|
||||
gap: 0.28rem;
|
||||
min-width: 0;
|
||||
border-radius: 0.75rem;
|
||||
background: rgb(255 255 255 / 0.025);
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.laboratory-summary__brief dt,
|
||||
.laboratory-result-conclusion dt {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.54rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.laboratory-summary__brief dd,
|
||||
.laboratory-result-conclusion dd {
|
||||
margin: 0;
|
||||
color: var(--nodedc-text-secondary);
|
||||
font-size: 0.64rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.laboratory-result-conclusion {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.laboratory-result-conclusion > div {
|
||||
display: grid;
|
||||
gap: 0.28rem;
|
||||
min-width: 0;
|
||||
border-top: 1px solid rgb(255 255 255 / 0.08);
|
||||
padding: 0.75rem 0.1rem 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.laboratory-summary__brief {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.laboratory-result-conclusion {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,8 @@
|
||||
.laboratory-summary p,
|
||||
.laboratory-summary dl,
|
||||
.laboratory-result-summary h2,
|
||||
.laboratory-result-summary p {
|
||||
.laboratory-result-summary p,
|
||||
.laboratory-result-summary dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -219,6 +219,16 @@ function SpatialWorkspace({
|
||||
const pointCloudFocused = Boolean(
|
||||
pointCloudSource && observationLayout.focusedSourceId === pointCloudSource.id,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!pointCloudFocused) return;
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== "Escape") return;
|
||||
event.preventDefault();
|
||||
observationLayout.setFocusedSourceId(null);
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [observationLayout.setFocusedSourceId, pointCloudFocused]);
|
||||
const floatingSourceMaximized = !unifiedPerception &&
|
||||
Boolean(observationLayout.maximizedFloatingSourceId);
|
||||
const timeline = state?.observationTimeline;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
@@ -36,6 +36,12 @@ export function E31Result({
|
||||
},
|
||||
{ label: "Контур", value: "Source-scoped · read-only" },
|
||||
]}
|
||||
brief={{
|
||||
question: "Можно ли однозначно связать camera, LiDAR и pose всей записи RAVNOVES00 без подбора соответствий вручную?",
|
||||
approach: "Для каждого кадра проверены временная привязка, чувствительность к offset, доступность pose и self-mask на одном неизменяемом источнике.",
|
||||
principalResult: `${formatNumber(metrics.availableBindingCount, 0)} из ${formatNumber(metrics.frameCount, 0)} кадров получили воспроизводимую source-scoped привязку.`,
|
||||
limitation: "Время основано на host-arrival, профиль действителен только для этой записи и этого монтажа сенсоров.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
@@ -70,38 +76,38 @@ export function E31Result({
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<section className="laboratory-result-summary">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||
<h2>Привязка воспроизводима в границах этого источника</h2>
|
||||
</div>
|
||||
<StatusBadge tone="success">Source profile принят</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
<div>
|
||||
<span>Camera ↔ LiDAR p95</span>
|
||||
<strong>{metrics.lidarCameraP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс</strong>
|
||||
<small>host-arrival binding</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Pose ↔ point p95</span>
|
||||
<strong>{metrics.posePointP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс</strong>
|
||||
<small>по доступным кадрам</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Поддержка соответствий</span>
|
||||
<strong>{(metrics.supportedFraction * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%</strong>
|
||||
<small>{formatNumber(metrics.correspondenceCount, 0)} соответствий</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Self-mask</span>
|
||||
<strong>{formatNumber(metrics.semanticSelfSampleCount, 0)} образцов</strong>
|
||||
<small>{formatNumber(metrics.semanticSelfCollateralCount, 0)} collateral</small>
|
||||
</div>
|
||||
</div>
|
||||
<p>Результат диагностический: он не переносится на другой монтаж сенсоров и не выдаёт командных полномочий.</p>
|
||||
</section>
|
||||
<LaboratoryResultSummary
|
||||
title="Привязка воспроизводима в границах этого источника"
|
||||
status="Source profile принят"
|
||||
statusTone="success"
|
||||
metrics={[
|
||||
{
|
||||
label: "Camera ↔ LiDAR p95",
|
||||
value: `${metrics.lidarCameraP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс`,
|
||||
hint: "host-arrival binding",
|
||||
},
|
||||
{
|
||||
label: "Pose ↔ point p95",
|
||||
value: `${metrics.posePointP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} мс`,
|
||||
hint: "по доступным кадрам",
|
||||
},
|
||||
{
|
||||
label: "Поддержка соответствий",
|
||||
value: `${(metrics.supportedFraction * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`,
|
||||
hint: `${formatNumber(metrics.correspondenceCount, 0)} соответствий`,
|
||||
},
|
||||
{
|
||||
label: "Self-mask",
|
||||
value: `${formatNumber(metrics.semanticSelfSampleCount, 0)} образцов`,
|
||||
hint: `${formatNumber(metrics.semanticSelfCollateralCount, 0)} collateral`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Camera, LiDAR и pose воспроизводимо связаны для неизменяемой записи RAVNOVES00 в заявленных границах source profile.",
|
||||
notProved: "Не доказаны аппаратная синхронизация, переносимость профиля на другую запись или другой монтаж и пригодность результата для управления.",
|
||||
decision: "Принять source-scoped профиль как диагностический вход E32; команды, навигация и safety остаются запрещены.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
@@ -33,6 +33,12 @@ export function E32Result({
|
||||
{ label: "Кадры", value: formatNumber(metrics.framesTotal, 0) },
|
||||
{ label: "Контур", value: "Diagnostic replay · read-only" },
|
||||
]}
|
||||
brief={{
|
||||
question: "Можно ли детерминированно превратить полную принятую запись E29/E31 в покадровый TrackGeometry без повторной настройки порогов?",
|
||||
approach: "Для каждого кадра применены один source profile, единый арбитраж перекрывающихся наблюдений, self-mask и запрет публикации диапазонов без квалифицированного источника.",
|
||||
principalResult: `TrackGeometry построен для ${formatNumber(metrics.framesTotal, 0)} кадров; опубликовано ${formatNumber(metrics.qualifiedPointsPublished, 0)} квалифицированных точек.`,
|
||||
limitation: "Результат наследует качество входных наблюдений и не считает отсутствие LiDAR-точек доказательством свободного пространства.",
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
@@ -67,38 +73,38 @@ export function E32Result({
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<section className="laboratory-result-summary">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||
<h2>TrackGeometry построен без повторной настройки порогов</h2>
|
||||
</div>
|
||||
<StatusBadge tone="success">Детерминированный replay</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
<div>
|
||||
<span>Опубликовано точек</span>
|
||||
<strong>{formatNumber(metrics.qualifiedPointsPublished, 0)}</strong>
|
||||
<small>{formatNumber(metrics.qualifiedPointsWithheld, 0)} withheld</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Арбитраж наблюдений</span>
|
||||
<strong>{formatNumber(metrics.observationsArbitrated, 0)}</strong>
|
||||
<small>{formatNumber(metrics.overlappingClaimsRemoved, 0)} overlap снято</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Semantic</span>
|
||||
<strong>{formatNumber(metrics.semanticPublished, 0)}</strong>
|
||||
<small>{formatNumber(metrics.semanticMasked, 0)} self-mask</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Frame processing p95</span>
|
||||
<strong>{metrics.frameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс</strong>
|
||||
<small>{(metrics.buildElapsedMs / 1000).toLocaleString("ru-RU", { maximumFractionDigits: 2 })} с build</small>
|
||||
</div>
|
||||
</div>
|
||||
<p>Отсутствие LiDAR-точек по-прежнему не трактуется как свободное пространство; результат не является навигационным ground truth.</p>
|
||||
</section>
|
||||
<LaboratoryResultSummary
|
||||
title="TrackGeometry построен без повторной настройки порогов"
|
||||
status="Детерминированный replay"
|
||||
statusTone="success"
|
||||
metrics={[
|
||||
{
|
||||
label: "Опубликовано точек",
|
||||
value: formatNumber(metrics.qualifiedPointsPublished, 0),
|
||||
hint: `${formatNumber(metrics.qualifiedPointsWithheld, 0)} withheld`,
|
||||
},
|
||||
{
|
||||
label: "Арбитраж наблюдений",
|
||||
value: formatNumber(metrics.observationsArbitrated, 0),
|
||||
hint: `${formatNumber(metrics.overlappingClaimsRemoved, 0)} overlap снято`,
|
||||
},
|
||||
{
|
||||
label: "Semantic",
|
||||
value: formatNumber(metrics.semanticPublished, 0),
|
||||
hint: `${formatNumber(metrics.semanticMasked, 0)} self-mask`,
|
||||
},
|
||||
{
|
||||
label: "Frame processing p95",
|
||||
value: `${metrics.frameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс`,
|
||||
hint: `${(metrics.buildElapsedMs / 1000).toLocaleString("ru-RU", { maximumFractionDigits: 2 })} с build`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Принятый source profile воспроизводимо материализует покадровый TrackGeometry с единственным владельцем перекрывающихся LiDAR-точек.",
|
||||
notProved: "Не доказаны полнота препятствий, свободное пространство при отсутствии точек и навигационная точность геометрии.",
|
||||
decision: "Принять TrackGeometry как неизменяемый диагностический вход worker shadow; не использовать как navigation ground truth.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
LaboratorySummary,
|
||||
LaboratoryWorkTemplate,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
@@ -23,33 +23,52 @@ export function E33Result({
|
||||
<LaboratoryWorkTemplate
|
||||
summary={(
|
||||
<LaboratorySummary
|
||||
title="LAB E33 · worker shadow 1×"
|
||||
description="Полная запись TrackGeometry выполнена отдельным bounded worker в темпе исходника 1×. Проверены очереди, доставка каждого кадра, задержка результата и ресурсы без подключения командного канала."
|
||||
status="Worker gate пройден"
|
||||
title="LAB E33 · проверка TrackGeometry worker в реальном темпе"
|
||||
description="Проверяли не качество распознавания объектов, а способность отдельного worker принять уже готовый TrackGeometry и вернуть явный результат по каждому кадру без накопления задержки и скрытых потерь."
|
||||
status="Принят как worker shadow"
|
||||
statusTone="success"
|
||||
facts={[
|
||||
{ label: "Конфигурация", value: `${rigLabel} · worker shadow` },
|
||||
{ label: "Worker", value: result.worker.node },
|
||||
{ label: "Доставка", value: `${formatNumber(metrics.deliveredFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)} кадров` },
|
||||
{ label: "Контур", value: "Recorded-source-paced · 1×" },
|
||||
{ label: "Источник", value: `${rigLabel} · RAVNOVES00` },
|
||||
{
|
||||
label: "Объём проверки",
|
||||
value: `${formatNumber(metrics.sourceFrames, 0)} кадров · ${metrics.sourceSpanSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} с`,
|
||||
},
|
||||
{
|
||||
label: "Геометрия доступна",
|
||||
value: `${formatNumber(metrics.sourceAvailableFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)} кадров`,
|
||||
},
|
||||
{ label: "Полномочия", value: "Read-only · команды и safety выключены" },
|
||||
]}
|
||||
brief={{
|
||||
question: "Сможет ли отдельный worker обработать всю запись в исходном темпе 1×, не теряя кадры и не раздувая очереди?",
|
||||
approach: "Готовый TrackGeometry из E32 выпускался по исходным временным меткам через отдельные work/result очереди ёмкостью 2. Для каждого кадра фиксировался один терминальный исход, задержка и использование памяти.",
|
||||
principalResult: `Да. Доставлены все ${formatNumber(metrics.deliveredFrames, 0)} исходов, замен и deadline miss не было; p95 возраста результата — ${metrics.resultAgeP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс.`,
|
||||
limitation: `Детектор, semantic-модели и качество геометрии здесь не пересчитывались. В ${formatNumber(metrics.sourceUnavailableFrames, 0)} кадрах источник уже не содержал квалифицированной геометрии.`,
|
||||
}}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "deterministic",
|
||||
pipelineId: result.pipelineId,
|
||||
pipelineId: "TrackGeometry → recorded-paced bounded worker",
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: result.e32ResultId,
|
||||
version: "accepted TrackGeometry replay",
|
||||
role: "immutable frame stream",
|
||||
identitySha256: result.e32ResultId.split("-").at(-1) ?? null,
|
||||
name: "Принятый TrackGeometry из LAB E32",
|
||||
version: `${formatNumber(metrics.sourceFrames, 0)} кадров · неизменяемая запись`,
|
||||
role: "готовый вход; detector и semantic inference не запускались повторно",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "Recorded pacing + bounded latest-wins queues",
|
||||
version: `1× · очереди ${metrics.workQueueCapacity}/${metrics.resultQueueCapacity}`,
|
||||
role: "выпуск по исходному времени, закрытый учёт исходов и deadline-контроль",
|
||||
identitySha256: null,
|
||||
},
|
||||
{
|
||||
kind: "runtime",
|
||||
name: result.worker.node,
|
||||
version: `${result.worker.python} · NumPy ${result.worker.numpy}`,
|
||||
role: "bounded worker, work/result queues 2/2",
|
||||
name: "Изолированный worker",
|
||||
version: `${result.worker.node} · Python ${result.worker.python} · NumPy ${result.worker.numpy}`,
|
||||
role: "проверка структуры TrackGeometry, SHA-256 диапазонов PointSlab и публикация результата",
|
||||
identitySha256: null,
|
||||
},
|
||||
],
|
||||
@@ -59,7 +78,7 @@ export function E33Result({
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ИСХОДНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Запись, доставленная worker без потерь"
|
||||
title="Контрольная запись: кадры и TrackGeometry, которые доставлял worker"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
@@ -67,40 +86,38 @@ export function E33Result({
|
||||
</LaboratoryEvidence>
|
||||
)}
|
||||
result={(
|
||||
<section className="laboratory-result-summary">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
|
||||
<h2>Полный replay выдержан в темпе источника</h2>
|
||||
</div>
|
||||
<StatusBadge tone="success">
|
||||
{formatNumber(metrics.deliveredFrames, 0)} / {formatNumber(metrics.sourceFrames, 0)}
|
||||
</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-result-metrics">
|
||||
<div>
|
||||
<span>Эффективная частота</span>
|
||||
<strong>{metrics.effectiveDeliveryFps.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} FPS</strong>
|
||||
<small>скорость источника 1×</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Result age p95</span>
|
||||
<strong>{metrics.resultAgeP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс</strong>
|
||||
<small>{(metrics.deadlineMissFraction * 100).toLocaleString("ru-RU", { maximumFractionDigits: 2 })}% deadline miss</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Processing p95</span>
|
||||
<strong>{metrics.processingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс</strong>
|
||||
<small>{metrics.releaseLagP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс release lag</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Ресурсы p95</span>
|
||||
<strong>{metrics.processRssP95Mib.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} MiB</strong>
|
||||
<small>GPU {metrics.gpuUtilizationP95Percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%</small>
|
||||
</div>
|
||||
</div>
|
||||
<p>Очереди остались bounded, пропусков нет. Результат подтверждает только worker shadow и не даёт навигационных или safety-полномочий.</p>
|
||||
</section>
|
||||
<LaboratoryResultSummary
|
||||
title="Worker выдержал запись; качество восприятия этим прогоном не проверялось"
|
||||
status="Worker shadow принят"
|
||||
statusTone="success"
|
||||
metrics={[
|
||||
{
|
||||
label: "Доставка исходов",
|
||||
value: `${formatNumber(metrics.deliveredFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)}`,
|
||||
hint: "0 замен · 0 пропусков",
|
||||
},
|
||||
{
|
||||
label: "Темп обработки",
|
||||
value: `${metrics.effectiveDeliveryFps.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} FPS`,
|
||||
hint: "исходная скорость 10 FPS · 1×",
|
||||
},
|
||||
{
|
||||
label: "Возраст результата",
|
||||
value: `${metrics.resultAgeP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс p95`,
|
||||
hint: `${metrics.resultAgeMaxMs.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс max · лимит ${formatNumber(metrics.resultDeadlineMs, 0)} мс`,
|
||||
},
|
||||
{
|
||||
label: "Память процесса",
|
||||
value: `${metrics.processRssMaxMib.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} MiB max`,
|
||||
hint: `лимит ${formatNumber(metrics.processRssLimitMib, 0)} MiB`,
|
||||
},
|
||||
]}
|
||||
conclusion={{
|
||||
proved: "Финальная стадия публикации TrackGeometry на этом worker успевает за данной записью: очереди ограничены, каждый кадр учтён, задержка остаётся ниже заявленного deadline.",
|
||||
notProved: "Прогон не измеряет точность машин, кубов или сегментации, не запускает заново detector/semantic pipeline и не доказывает переносимость на другую запись или монтаж сенсоров.",
|
||||
decision: "Результат принимается только как диагностический worker shadow. Он допускает дальнейшие лабораторные испытания временного occupied/unknown слоя, но не включает команды, навигацию или safety.",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -44,8 +44,11 @@ import type { WorkspaceRendererProps } from "../contracts";
|
||||
import { E31Result } from "./E31Result";
|
||||
import { E32Result } from "./E32Result";
|
||||
import { E33Result } from "./E33Result";
|
||||
import {
|
||||
e28LaboratoryBrief, e29LaboratoryBrief,
|
||||
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
|
||||
} from "./laboratoryArchiveBriefs";
|
||||
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
|
||||
|
||||
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
|
||||
SpatialView: ComponentType<WorkspaceRendererProps>;
|
||||
};
|
||||
@@ -227,6 +230,7 @@ function E29LaboratoryResult({
|
||||
value: "Read-only · hash verified",
|
||||
},
|
||||
]}
|
||||
brief={e29LaboratoryBrief(formatNumber(semantic.total, 0))}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "hybrid",
|
||||
@@ -411,6 +415,7 @@ function E30LaboratoryResult({
|
||||
{ label: "Кейсов", value: formatNumber(result.itemCount, 0) },
|
||||
{ label: "Контур", value: "Read-only · без LAB publish" },
|
||||
]}
|
||||
brief={e30LaboratoryBrief(formatNumber(result.itemCount, 0))}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
@@ -483,6 +488,7 @@ function PublishedLaboratoryResult({
|
||||
{ label: "Конфигурация", value: lab?.configSha256?.slice(0, 16) ?? "Не зафиксирована" },
|
||||
{ label: "Контур", value: "Диагностика · без команд" },
|
||||
]}
|
||||
brief={PUBLISHED_LABORATORY_BRIEF}
|
||||
method={publishedLaboratoryMethod(session)}
|
||||
/>
|
||||
)}
|
||||
@@ -867,6 +873,10 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|
||||
{ label: "Режим", value: "Recorded-source-paced shadow" },
|
||||
{ label: "Контур", value: "Read-only · hash verified" },
|
||||
]}
|
||||
brief={e28LaboratoryBrief(
|
||||
formatNumber(e28Model?.metrics.frames.valid ?? 0, 0),
|
||||
formatNumber(e28Model?.metrics.frames.total ?? 0, 0),
|
||||
)}
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: e28Model?.method.executionClass ?? "deterministic",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type {
|
||||
LaboratoryBrief,
|
||||
} from "../../components/laboratory/LaboratoryPresentation";
|
||||
|
||||
export function e28LaboratoryBrief(
|
||||
validFrames: string,
|
||||
totalFrames: string,
|
||||
): LaboratoryBrief {
|
||||
return {
|
||||
question: "Можно ли устойчиво оценивать локальную поверхность и временные остатки на полной записи без изменения источника?",
|
||||
approach: "LiDAR и pose воспроизводятся через bounded recorded-source-paced shadow; rolling local plane оценивает поверхность, препятствия и temporal residuals.",
|
||||
principalResult: `${validFrames} из ${totalFrames} кадров прошли диагностическую обработку L2.6.`,
|
||||
limitation: "Модель проверена только на RAVNOVES00 и не получает командных, навигационных или safety-полномочий.",
|
||||
};
|
||||
}
|
||||
|
||||
export function e29LaboratoryBrief(
|
||||
observationCount: string,
|
||||
): LaboratoryBrief {
|
||||
return {
|
||||
question: "Совпадают ли camera-семантика объекта и независимая LiDAR-геометрия в одном точном кадре?",
|
||||
approach: "Camera сохраняет класс и идентичность, а локальная поверхность L2.6 отдельно проверяет дальность, occupied support и конфликт принадлежности точек.",
|
||||
principalResult: `${observationCount} наблюдений разложены по согласованию camera и geometry без объявления пустого LiDAR свободным пространством.`,
|
||||
limitation: "Результат зависит от точности исходной camera-семантики и локальной поверхности; он остаётся диагностическим и read-only.",
|
||||
};
|
||||
}
|
||||
|
||||
export function e30LaboratoryBrief(
|
||||
itemCount: string,
|
||||
): LaboratoryBrief {
|
||||
return {
|
||||
question: "Можно ли человеку проверить спорный camera/LiDAR-кейс по понятному совмещённому доказательству, а не по изолированному облаку точек?",
|
||||
approach: "Каждый кейс связан с точным camera frame, переключаемой LiDAR-проекцией, frame-local индексами и синхронным 3D в одном viewer.",
|
||||
principalResult: `${itemCount} кейсов доступны для camera-first проверки и отдельного разбора только неоднозначных исключений.`,
|
||||
limitation: "Рабочее место не меняет исходный A2 и не превращает решение оператора в navigation или safety ground truth.",
|
||||
};
|
||||
}
|
||||
|
||||
export const PUBLISHED_LABORATORY_BRIEF: LaboratoryBrief = {
|
||||
question: "Что было зафиксировано опубликованной лабораторной работой на её неизменяемом источнике?",
|
||||
approach: "Mission Core открывает связанный source replay и проецирует только опубликованные method/provenance и метрики без реконструкции отсутствующих данных.",
|
||||
principalResult: "Оператор получает исходное визуальное доказательство и зафиксированный результат одного LAB run в общей продуктовой форме.",
|
||||
limitation: "Для legacy-результатов часть идентичностей и метрик могла не публиковаться; приложение их не выдумывает задним числом.",
|
||||
};
|
||||
Reference in New Issue
Block a user