feat(polygon): add full-frame operator review

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 16:45:10 +03:00
parent cea63f9a1e
commit b037076506
10 changed files with 2080 additions and 330 deletions
@@ -63,6 +63,36 @@ export interface GroundFailurePreview {
patchworkDisagreement: number[];
}
export interface GroundReviewFrameMetrics {
groundIou: number;
naturalGroundRecall: number;
obstacleNonGroundRecall: number;
latencyMs: number;
}
export interface GroundReviewFrameSummary {
sequence: number;
frameId: string;
sourcePointCount: number;
pointCount: number;
current: GroundReviewFrameMetrics;
patchwork: GroundReviewFrameMetrics;
groundIouDelta: number;
}
export interface GroundReview {
runId: string;
identitySha256: string;
sourceId: string;
frameCount: number;
previewPoints: number;
frames: GroundReviewFrameSummary[];
}
export interface GroundReviewFrame extends GroundFailurePreview {
intensity0To255: number[];
}
export class GroundQualificationContractError extends Error {}
export class GroundQualificationApiError extends Error {
@@ -130,6 +160,14 @@ function boolean(value: unknown, field: string): boolean {
return value;
}
function integer(value: unknown, field: string, minimum = 0): number {
const result = number(value, field, minimum);
if (!Number.isInteger(result)) {
throw new GroundQualificationContractError(`${field} должен быть целым числом.`);
}
return result;
}
function aggregate(value: unknown, field: string): QualificationMetricAggregate {
const source = record(value, field);
const micro = record(source.micro, `${field}.micro`);
@@ -316,6 +354,113 @@ export function decodeGroundFailurePreview(payload: unknown): GroundFailurePrevi
};
}
function reviewFrameMetrics(value: unknown, field: string): GroundReviewFrameMetrics {
const source = record(value, field);
return {
groundIou: fraction(source.ground_iou, `${field}.ground_iou`),
naturalGroundRecall: fraction(
source.natural_ground_recall,
`${field}.natural_ground_recall`,
),
obstacleNonGroundRecall: fraction(
source.obstacle_non_ground_recall,
`${field}.obstacle_non_ground_recall`,
),
latencyMs: number(source.latency_ms, `${field}.latency_ms`),
};
}
export function decodeGroundReview(payload: unknown): GroundReview {
const source = record(payload, "ground review");
if (
source.schema_version !== "missioncore.polygon-ground-review/v1"
|| source.access !== "read-only"
) {
throw new GroundQualificationContractError("Неизвестная схема покадрового просмотра.");
}
const frameCount = integer(source.frame_count, "frame_count", 1);
const frames = array(source.frames, "frames", 2_000).map(
(value, index): GroundReviewFrameSummary => {
const frame = record(value, `frames[${index}]`);
const sequence = integer(frame.sequence, `frames[${index}].sequence`);
if (sequence !== index) {
throw new GroundQualificationContractError("Кадры просмотра идут не по порядку.");
}
return {
sequence,
frameId: id(frame.frame_id, `frames[${index}].frame_id`),
sourcePointCount: integer(
frame.source_point_count,
`frames[${index}].source_point_count`,
1,
),
pointCount: integer(frame.point_count, `frames[${index}].point_count`, 1),
current: reviewFrameMetrics(frame.current, `frames[${index}].current`),
patchwork: reviewFrameMetrics(
frame.patchworkpp,
`frames[${index}].patchworkpp`,
),
groundIouDelta: number(
frame.ground_iou_delta,
`frames[${index}].ground_iou_delta`,
-1,
),
};
},
);
if (frames.length !== frameCount) {
throw new GroundQualificationContractError("Индекс просмотра неполный.");
}
frames.forEach((frame, index) => {
if (frame.pointCount > frame.sourcePointCount) {
throw new GroundQualificationContractError(
`frames[${index}] содержит больше preview-точек, чем source-точек.`,
);
}
});
const identitySha256 = text(source.identity_sha256, "identity_sha256", 64);
if (!SHA256.test(identitySha256)) {
throw new GroundQualificationContractError("Review identity должен содержать SHA-256.");
}
return {
runId: id(source.run_id, "run_id"),
identitySha256,
sourceId: text(source.source_id, "source_id"),
frameCount,
previewPoints: integer(source.preview_points, "preview_points", 1),
frames,
};
}
export function decodeGroundReviewFrame(payload: unknown): GroundReviewFrame {
const source = record(payload, "ground review frame");
if (
source.schema_version !== "missioncore.polygon-ground-review-frame/v1"
|| source.access !== "read-only"
) {
throw new GroundQualificationContractError("Неизвестная схема кадра просмотра.");
}
const compatible = {
...source,
schema_version: "missioncore.polygon-ground-failure-preview/v1",
};
const decoded = decodeGroundFailurePreview(compatible);
const intensity0To255 = array(
source.intensity_0_255,
"intensity_0_255",
50_000,
).map((value, index) => integer(value, `intensity_0_255[${index}]`));
if (
intensity0To255.length !== decoded.pointCount
|| intensity0To255.some((value) => value > 255)
) {
throw new GroundQualificationContractError(
"intensity_0_255 не совпадает с point_count.",
);
}
return { ...decoded, intensity0To255 };
}
async function requestJson(
url: string,
signal: AbortSignal | undefined,
@@ -391,3 +536,48 @@ export async function fetchGroundFailurePreview(
),
);
}
export async function fetchGroundReview(
runId: string,
{
signal,
fetcher = globalThis.fetch,
}: { signal?: AbortSignal; fetcher?: QualificationFetch } = {},
): Promise<GroundReview | null> {
if (!SAFE_ID.test(runId)) {
throw new GroundQualificationContractError("Недопустимый run_id.");
}
try {
return decodeGroundReview(
await requestJson(
`/api/v1/polygon/runs/${encodeURIComponent(runId)}/qualification/review`,
signal,
fetcher,
),
);
} catch (error) {
if (error instanceof GroundQualificationApiError && error.status === 404) return null;
throw error;
}
}
export async function fetchGroundReviewFrame(
runId: string,
frameId: string,
{
signal,
fetcher = globalThis.fetch,
}: { signal?: AbortSignal; fetcher?: QualificationFetch } = {},
): Promise<GroundReviewFrame> {
if (!SAFE_ID.test(runId) || !SAFE_ID.test(frameId)) {
throw new GroundQualificationContractError("Недопустимый идентификатор кадра.");
}
return decodeGroundReviewFrame(
await requestJson(
`/api/v1/polygon/runs/${encodeURIComponent(runId)}/qualification/review/frames/`
+ encodeURIComponent(frameId),
signal,
fetcher,
),
);
}
+1 -1
View File
@@ -156,7 +156,7 @@ export const workspaces: WorkspaceDefinition[] = [
label: "Прогоны",
title: "Прогоны",
eyebrow: "ПОЛИГОН / ПРОГОНЫ",
description: "Live Simulation Worker, история и доказательства квалификационных прогонов.",
description: "Покадровый replay, сравнение алгоритмов и история квалификационных прогонов.",
icon: "activity",
kind: "polygon-run",
groups: [],
@@ -97,6 +97,16 @@
}
@media (max-width: 1040px) {
.polygon-review-summary > div:last-child,
.polygon-review-analysis__content,
.polygon-run-technical__content {
grid-template-columns: 1fr;
}
.polygon-review-frame-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.polygon-qualification__grid,
.polygon-qualification__failures {
grid-template-columns: 1fr;
@@ -183,6 +193,42 @@
}
@media (max-width: 760px) {
.polygon-review-lead,
.polygon-review-player__header,
.polygon-review-order,
.polygon-review-summary > header {
align-items: stretch;
flex-direction: column;
}
.polygon-review-lead > div:last-child {
justify-content: space-between;
}
.polygon-review-mode {
justify-content: flex-start;
}
.polygon-review-frame-metrics {
grid-template-columns: 1fr;
}
.polygon-review-controls {
grid-template-columns: auto auto auto minmax(0, 1fr);
}
.polygon-review-controls > span {
display: none;
}
.polygon-review-summary article {
grid-template-columns: minmax(0, 1fr) auto auto;
}
.polygon-review-summary article small {
grid-column: 1 / -1;
}
.polygon-qualification__heading,
.polygon-qualification__viewer > header {
display: grid;
@@ -1015,6 +1015,504 @@
font-size: 0.62rem;
}
.polygon-review-lead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1.5rem;
padding: 0.15rem 0.1rem 0.25rem;
}
.polygon-review-lead h2,
.polygon-review-lead p {
margin: 0;
}
.polygon-review-lead h2 {
margin-top: 0.32rem;
color: var(--nodedc-text-primary);
font-size: 1.35rem;
letter-spacing: -0.04em;
}
.polygon-review-lead p {
margin-top: 0.32rem;
color: var(--nodedc-text-muted);
font-size: 0.66rem;
}
.polygon-review-lead > div:last-child {
display: flex;
align-items: center;
gap: 0.55rem;
}
.polygon-review-lead select {
max-width: 18rem;
border: 0;
border-radius: 999px;
outline: 0;
background: rgb(255 255 255 / 0.06);
color: var(--nodedc-text-secondary);
padding: 0.42rem 0.75rem;
font-size: 0.58rem;
}
.polygon-review-player {
overflow: hidden;
border-radius: 1.15rem;
background: var(--station-panel);
}
.polygon-review-player__header {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.85rem 1rem;
}
.polygon-review-player__header > div:first-child {
display: grid;
min-width: 0;
gap: 0.25rem;
}
.polygon-review-player__header strong {
overflow: hidden;
max-width: 36rem;
color: var(--nodedc-text-primary);
font-size: 0.67rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-review-mode,
.polygon-review-order > div {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.28rem;
}
.polygon-review-mode button,
.polygon-review-order button,
.polygon-review-controls button {
border: 0;
border-radius: 999px;
outline: 0;
background: transparent;
color: var(--nodedc-text-muted);
padding: 0.42rem 0.68rem;
font: inherit;
font-size: 0.57rem;
cursor: pointer;
}
.polygon-review-mode button:hover,
.polygon-review-mode button[data-active="true"],
.polygon-review-order button:hover,
.polygon-review-order button[data-active="true"],
.polygon-review-controls button:hover:not(:disabled) {
outline: 0;
background: rgb(255 255 255 / 0.1);
color: var(--nodedc-text-primary);
}
.polygon-review-stage {
position: relative;
background: #06070a;
}
.polygon-review-stage .lidar-ground-scene {
min-height: min(56rem, 62vh);
border-radius: 0;
}
.polygon-review-stage__loading {
display: grid;
min-height: min(56rem, 62vh);
place-items: center;
color: var(--nodedc-text-muted);
font-size: 0.66rem;
}
.polygon-review-stage__legend {
position: absolute;
z-index: 3;
top: 0.8rem;
left: 0.8rem;
display: grid;
max-width: 25rem;
gap: 0.18rem;
border-radius: 0.75rem;
background: rgb(7 8 10 / 0.76);
padding: 0.55rem 0.68rem;
backdrop-filter: blur(14px);
}
.polygon-review-stage__legend strong {
color: var(--nodedc-text-primary);
font-size: 0.62rem;
}
.polygon-review-stage__legend span {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.polygon-review-frame-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.28rem;
padding: 0.38rem;
}
.polygon-review-frame-metrics article {
display: grid;
min-width: 0;
gap: 0.24rem;
border-radius: 0.82rem;
background: rgb(255 255 255 / 0.035);
padding: 0.72rem 0.78rem;
}
.polygon-review-frame-metrics span,
.polygon-review-frame-metrics small {
overflow: hidden;
color: var(--nodedc-text-muted);
font-size: 0.54rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-review-frame-metrics strong {
color: var(--nodedc-text-primary);
font-size: 1.02rem;
font-weight: 680;
}
.polygon-review-frame-metrics article[data-positive="false"] strong {
color: rgb(var(--nodedc-danger-rgb));
}
.polygon-review-controls {
display: grid;
grid-template-columns: auto auto auto minmax(8rem, 1fr) auto;
align-items: center;
gap: 0.38rem;
padding: 0.3rem 0.55rem 0.15rem;
}
.polygon-review-controls button {
background: rgb(255 255 255 / 0.055);
color: var(--nodedc-text-secondary);
}
.polygon-review-controls button:disabled {
opacity: 0.3;
cursor: default;
}
.polygon-review-controls input {
width: 100%;
accent-color: #f0f0ec;
}
.polygon-review-controls > span {
min-width: 4.5rem;
color: var(--nodedc-text-muted);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 0.55rem;
text-align: right;
}
.polygon-review-order {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.2rem 0.55rem 0.45rem;
}
.polygon-review-order > div:last-child button {
color: var(--nodedc-text-secondary);
}
.polygon-review-timeline {
display: flex;
height: 2.4rem;
align-items: stretch;
gap: 1px;
overflow: hidden;
margin: 0 0.65rem;
border-radius: 0.42rem;
background: rgb(255 255 255 / 0.025);
padding: 0.32rem 0;
}
.polygon-review-timeline button {
min-width: 1px;
flex: 1 1 1px;
border: 0;
border-radius: 0;
outline: 0;
background: var(--frame-color);
padding: 0;
cursor: pointer;
opacity: 0.62;
}
.polygon-review-timeline button:hover,
.polygon-review-timeline button[data-active="true"] {
min-width: 3px;
opacity: 1;
box-shadow: 0 0 0 1px rgb(255 255 255 / 0.86);
}
.polygon-review-timeline-note {
margin: 0;
padding: 0.25rem 0.75rem 0.78rem;
color: var(--nodedc-text-muted);
font-size: 0.54rem;
line-height: 1.45;
}
.polygon-review-summary,
.polygon-review-unavailable,
.polygon-review-analysis,
.polygon-run-technical {
border-radius: 1rem;
background: var(--station-panel);
}
.polygon-review-summary {
display: grid;
gap: 0.7rem;
padding: 0.9rem 1rem;
}
.polygon-review-summary > header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.polygon-review-summary h3,
.polygon-review-summary p {
margin: 0;
}
.polygon-review-summary h3 {
margin-top: 0.28rem;
color: var(--nodedc-text-primary);
font-size: 0.94rem;
}
.polygon-review-summary > p {
max-width: 52rem;
color: var(--nodedc-text-muted);
font-size: 0.62rem;
line-height: 1.55;
}
.polygon-review-summary > div:last-child {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.38rem;
}
.polygon-review-summary article {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto auto;
align-items: center;
gap: 0.58rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.03);
padding: 0.65rem 0.72rem;
}
.polygon-review-summary article span,
.polygon-review-summary article small {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.polygon-review-summary article strong {
color: var(--nodedc-text-primary);
font-size: 0.72rem;
}
.polygon-review-summary article i {
color: var(--nodedc-text-muted);
font-style: normal;
}
.polygon-review-analysis,
.polygon-run-technical {
overflow: hidden;
}
.polygon-review-analysis > summary,
.polygon-run-technical > summary {
padding: 0.8rem 0.95rem;
color: var(--nodedc-text-secondary);
font-size: 0.62rem;
cursor: pointer;
list-style: none;
}
.polygon-review-analysis > summary::-webkit-details-marker,
.polygon-run-technical > summary::-webkit-details-marker {
display: none;
}
.polygon-review-analysis > summary::after,
.polygon-run-technical > summary::after {
float: right;
content: "";
color: var(--nodedc-text-muted);
}
.polygon-review-analysis[open] > summary::after,
.polygon-run-technical[open] > summary::after {
content: "";
}
.polygon-review-analysis__content {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 0.7rem;
padding: 0 0.65rem 0.65rem;
}
.polygon-review-analysis__content section {
display: grid;
align-content: start;
gap: 0.25rem;
border-radius: 0.8rem;
background: rgb(255 255 255 / 0.025);
padding: 0.75rem;
}
.polygon-review-analysis__content p {
display: grid;
grid-template-columns: auto minmax(0, 1fr) repeat(3, auto);
align-items: center;
gap: 0.45rem;
margin: 0;
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.polygon-review-analysis__content p i {
width: 0.38rem;
height: 0.38rem;
border-radius: 50%;
background: rgb(var(--nodedc-danger-rgb));
}
.polygon-review-analysis__content p[data-passed="true"] i {
background: rgb(var(--nodedc-success-rgb));
}
.polygon-review-analysis__content code {
color: var(--nodedc-text-muted);
font-size: 0.52rem;
}
.polygon-review-unavailable {
display: grid;
min-height: 18rem;
align-content: center;
gap: 0.45rem;
padding: 1rem;
}
.polygon-review-unavailable h3,
.polygon-review-unavailable p,
.polygon-review-unavailable small {
margin: 0;
}
.polygon-review-unavailable h3 {
color: var(--nodedc-text-primary);
font-size: 1rem;
}
.polygon-review-unavailable p,
.polygon-review-unavailable small {
max-width: 44rem;
color: var(--nodedc-text-muted);
font-size: 0.62rem;
line-height: 1.5;
}
.polygon-run-technical__content {
display: grid;
grid-template-columns: 1.2fr 1fr 1fr;
gap: 0.6rem;
padding: 0 0.65rem 0.65rem;
}
.polygon-run-technical__content > * {
min-width: 0;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.025);
padding: 0.7rem;
}
.polygon-run-technical__content dl {
display: grid;
gap: 0.3rem;
margin: 0;
}
.polygon-run-technical__content dl > div,
.polygon-run-technical__content section p {
display: flex;
min-width: 0;
justify-content: space-between;
gap: 0.65rem;
margin: 0;
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.polygon-run-technical__content dd {
overflow: hidden;
margin: 0;
color: var(--nodedc-text-secondary);
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-technical__content h4 {
margin: 0 0 0.55rem;
color: var(--nodedc-text-primary);
font-size: 0.62rem;
}
.polygon-run-technical__content section {
display: grid;
align-content: start;
gap: 0.4rem;
}
.polygon-run-technical__content section p strong,
.polygon-run-technical__content section p span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-technical__content section p strong {
color: var(--nodedc-text-secondary);
font-weight: 600;
}
.capability-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -1,15 +1,20 @@
import { useEffect, useMemo, useState } from "react";
import {
Button,
GlassSurface,
StatusBadge,
} from "@nodedc/ui-react";
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from "react";
import { Button, StatusBadge } from "@nodedc/ui-react";
import {
fetchGroundFailurePreview,
fetchGroundQualification,
type GroundFailurePreview,
fetchGroundReview,
fetchGroundReviewFrame,
type GroundQualification,
type GroundReview,
type GroundReviewFrame,
type GroundReviewFrameSummary,
} from "../core/polygon/groundQualification";
import {
fetchPolygonRunCatalog,
@@ -23,12 +28,13 @@ import {
LidarGroundPointCloud,
type LidarGroundViewMode,
} from "./LidarGroundPointCloud";
import { PolygonLivePanel } from "./PolygonLivePanel";
interface PolygonRunWorkspaceProps {
route: PolygonRunRoute;
}
type ReviewOrder = "sequence" | "best" | "worst";
const stateLabels: Record<PolygonRunState, string> = {
admitted: "Допущен",
starting: "Запускается",
@@ -40,6 +46,21 @@ const stateLabels: Record<PolygonRunState, string> = {
aborted: "Прерван",
};
const viewModes: ReadonlyArray<[LidarGroundViewMode, string]> = [
["intensity", "Облако"],
["ground-truth", "Эталон"],
["current", "Current"],
["candidate", "Patchwork++"],
["disagreement", "Ошибки Current"],
["candidate-disagreement", "Ошибки Patchwork++"],
];
const orderLabels: ReadonlyArray<[ReviewOrder, string]> = [
["sequence", "По записи"],
["best", "Лучшие"],
["worst", "Худшие"],
];
function stateTone(
state: PolygonRunState,
): "success" | "accent" | "warning" | "danger" | "neutral" {
@@ -69,7 +90,7 @@ function formatBytes(value: number): string {
function errorMessage(error: unknown): string {
if (error instanceof Error && error.message.trim()) return error.message;
return "Не удалось прочитать доказательства прогона.";
return "Не удалось прочитать прогон.";
}
function formatPercent(value: number): string {
@@ -79,10 +100,65 @@ function formatPercent(value: number): string {
}).format(value);
}
function formatDelta(value: number): string {
const points = value * 100;
return `${points >= 0 ? "+" : ""}${points.toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} п.п.`;
}
function formatMilliseconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 1 })} мс`;
}
function frameScene(frame: GroundReviewFrame | null) {
if (!frame) return null;
return {
pointCount: frame.pointCount,
pointsXyzM: frame.pointsXyzM,
intensity0To255: frame.intensity0To255,
masks: {
currentGround: frame.currentGround,
currentAssigned: frame.evaluated,
candidateGround: frame.patchworkGround,
candidateAssigned: frame.evaluated,
disagreement: frame.currentDisagreement,
candidateDisagreement: frame.patchworkDisagreement,
groundTruthGround: frame.groundTruthGround,
},
};
}
function modeExplanation(mode: LidarGroundViewMode): string {
if (mode === "intensity") return "Нейтральная геометрия нативного LiDAR-кадра.";
if (mode === "ground-truth") return "Публичная GOOSE-разметка: земля и всё остальное.";
if (mode === "current") return "Что текущий алгоритм Mission Core считает землёй.";
if (mode === "candidate") return "Что Patchwork++ считает землёй.";
if (mode === "disagreement") return "Красным — точки, где Current расходится с эталоном.";
return "Красным — точки, где Patchwork++ расходится с эталоном.";
}
function frameColor(delta: number): string {
if (delta < 0) {
const opacity = Math.min(0.92, 0.42 + Math.abs(delta) * 3);
return `rgb(255 104 104 / ${opacity})`;
}
const opacity = Math.min(0.9, 0.25 + delta * 2.8);
return `rgb(210 242 188 / ${opacity})`;
}
function orderFrames(
review: GroundReview,
order: ReviewOrder,
): GroundReviewFrameSummary[] {
if (order === "sequence") return review.frames;
return [...review.frames].sort((left, right) => (
order === "best"
? right.groundIouDelta - left.groundIouDelta
: left.groundIouDelta - right.groundIouDelta
));
}
export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const [catalog, setCatalog] = useState<PolygonRunCatalog | null>(null);
const [detail, setDetail] = useState<PolygonRunDetail | null>(null);
@@ -91,11 +167,14 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const [error, setError] = useState<string | null>(route.error);
const [reloadGeneration, setReloadGeneration] = useState(0);
const [qualification, setQualification] = useState<GroundQualification | null>(null);
const [qualificationError, setQualificationError] = useState<string | null>(null);
const [failurePreview, setFailurePreview] = useState<GroundFailurePreview | null>(null);
const [selectedFailureFrameId, setSelectedFailureFrameId] = useState<string | null>(null);
const [failureMode, setFailureMode] =
useState<LidarGroundViewMode>("candidate-disagreement");
const [review, setReview] = useState<GroundReview | null>(null);
const [reviewError, setReviewError] = useState<string | null>(null);
const [selectedFrameId, setSelectedFrameId] = useState<string | null>(null);
const [reviewFrame, setReviewFrame] = useState<GroundReviewFrame | null>(null);
const [reviewMode, setReviewMode] = useState<LidarGroundViewMode>("ground-truth");
const [reviewOrder, setReviewOrder] = useState<ReviewOrder>("sequence");
const [playing, setPlaying] = useState(false);
const frameCache = useRef(new Map<string, GroundReviewFrame>());
useEffect(() => {
if (route.error) {
@@ -135,79 +214,134 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
useEffect(() => {
const runId = detail?.run.runId;
if (!runId) {
setQualification(null);
setQualificationError(null);
return;
}
const controller = new AbortController();
setQualification(null);
setQualificationError(null);
setFailurePreview(null);
setSelectedFailureFrameId(null);
void fetchGroundQualification(runId, { signal: controller.signal })
.then((result) => {
setReview(null);
setReviewFrame(null);
setSelectedFrameId(null);
setReviewError(null);
setPlaying(false);
frameCache.current.clear();
if (!runId) return;
const controller = new AbortController();
void (async () => {
try {
const nextQualification = await fetchGroundQualification(runId, {
signal: controller.signal,
});
if (controller.signal.aborted) return;
setQualification(result);
setSelectedFailureFrameId(result?.worstFrames[0]?.frameId ?? null);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setQualificationError(errorMessage(loadError));
});
setQualification(nextQualification);
if (!nextQualification) return;
const nextReview = await fetchGroundReview(runId, {
signal: controller.signal,
});
if (controller.signal.aborted) return;
setReview(nextReview);
setSelectedFrameId(nextReview?.frames[0]?.frameId ?? null);
} catch (loadError) {
if (!controller.signal.aborted) setReviewError(errorMessage(loadError));
}
})();
return () => controller.abort();
}, [detail?.run.runId]);
useEffect(() => {
const runId = qualification?.runId;
if (!runId || !selectedFailureFrameId) {
setFailurePreview(null);
const runId = review?.runId;
if (!runId || !selectedFrameId) {
setReviewFrame(null);
return;
}
const cached = frameCache.current.get(selectedFrameId);
if (cached) {
setReviewFrame(cached);
return;
}
const controller = new AbortController();
setFailurePreview(null);
void fetchGroundFailurePreview(runId, selectedFailureFrameId, {
setReviewFrame(null);
void fetchGroundReviewFrame(runId, selectedFrameId, {
signal: controller.signal,
})
.then((result) => {
if (!controller.signal.aborted) setFailurePreview(result);
.then((frame) => {
if (controller.signal.aborted) return;
frameCache.current.set(frame.frameId, frame);
while (frameCache.current.size > 24) {
const oldest = frameCache.current.keys().next().value as string | undefined;
if (oldest) frameCache.current.delete(oldest);
else break;
}
setReviewFrame(frame);
})
.catch((loadError: unknown) => {
if (!controller.signal.aborted) setQualificationError(errorMessage(loadError));
if (!controller.signal.aborted) setReviewError(errorMessage(loadError));
});
return () => controller.abort();
}, [qualification?.runId, selectedFailureFrameId]);
}, [review?.runId, selectedFrameId]);
const visibleEvents = useMemo(
() => detail ? [...detail.events].reverse() : [],
[detail],
const orderedFrames = useMemo(
() => review ? orderFrames(review, reviewOrder) : [],
[review, reviewOrder],
);
const failureFrame = useMemo(() => {
if (!failurePreview) return null;
return {
pointCount: failurePreview.pointCount,
pointsXyzM: failurePreview.pointsXyzM,
intensity0To255: null,
masks: {
currentGround: failurePreview.currentGround,
currentAssigned: failurePreview.evaluated,
candidateGround: failurePreview.patchworkGround,
candidateAssigned: failurePreview.evaluated,
disagreement: failurePreview.currentDisagreement,
candidateDisagreement: failurePreview.patchworkDisagreement,
groundTruthGround: failurePreview.groundTruthGround,
},
};
}, [failurePreview]);
const selectedPosition = Math.max(
0,
orderedFrames.findIndex((frame) => frame.frameId === selectedFrameId),
);
const selectedSummary = orderedFrames[selectedPosition] ?? null;
const bestFrame = useMemo(
() => review
? [...review.frames].sort(
(left, right) => right.groundIouDelta - left.groundIouDelta,
)[0]
: null,
[review],
);
const worstFrame = useMemo(
() => review
? [...review.frames].sort(
(left, right) => left.groundIouDelta - right.groundIouDelta,
)[0]
: null,
[review],
);
useEffect(() => {
if (!playing || !reviewFrame || !selectedFrameId || !orderedFrames.length) return;
if (reviewFrame.frameId !== selectedFrameId) return;
const timer = window.setTimeout(() => {
const nextPosition = selectedPosition + 1;
if (nextPosition >= orderedFrames.length) {
setPlaying(false);
return;
}
setSelectedFrameId(orderedFrames[nextPosition].frameId);
}, 520);
return () => window.clearTimeout(timer);
}, [
orderedFrames,
playing,
reviewFrame,
selectedFrameId,
selectedPosition,
]);
const scene = useMemo(() => frameScene(reviewFrame), [reviewFrame]);
const selectPosition = (position: number) => {
const frame = orderedFrames[Math.max(0, Math.min(position, orderedFrames.length - 1))];
if (frame) setSelectedFrameId(frame.frameId);
};
const changeOrder = (order: ReviewOrder) => {
setPlaying(false);
setReviewOrder(order);
};
if (loading && !detail) {
return (
<div className="standard-workspace polygon-run-workspace">
<PolygonLivePanel />
<GlassSurface className="polygon-run-message" padding="lg">
<StatusBadge tone="accent">Только чтение</StatusBadge>
<h2>Проверяем журнал прогона</h2>
<p>Mission Core читает манифест, события и индекс артефактов без запуска провайдеров.</p>
</GlassSurface>
<section className="polygon-run-message">
<span className="section-eyebrow">ПОЛИГОН / ПРОГОНЫ</span>
<h2>Открываем запись</h2>
<p>Читаем индекс кадров и готовим покадровый просмотр.</p>
</section>
</div>
);
}
@@ -215,19 +349,18 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
if (error) {
return (
<div className="standard-workspace polygon-run-workspace">
<PolygonLivePanel />
<GlassSurface className="polygon-run-message" padding="lg">
<section className="polygon-run-message">
<StatusBadge tone="danger">Данные недоступны</StatusBadge>
<h2>UI-0 не может открыть прогон</h2>
<h2>Не удалось открыть прогон</h2>
<p>{error}</p>
<Button
size="compact"
variant="secondary"
onClick={() => setReloadGeneration((value) => value + 1)}
>
Повторить чтение
Повторить
</Button>
</GlassSurface>
</section>
</div>
);
}
@@ -235,12 +368,11 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
if (!detail) {
return (
<div className="standard-workspace polygon-run-workspace">
<PolygonLivePanel />
<GlassSurface className="polygon-run-message" padding="lg">
<StatusBadge tone="neutral">Журнал пуст</StatusBadge>
<h2>Квалификационных прогонов пока нет</h2>
<p>Экран появится автоматически после публикации первого журнала в read-only источник.</p>
</GlassSurface>
<section className="polygon-run-message">
<span className="section-eyebrow">ПОЛИГОН / ПРОГОНЫ</span>
<h2>Записей пока нет</h2>
<p>Здесь появятся replay- и simulation-прогоны после публикации результатов.</p>
</section>
</div>
);
}
@@ -248,82 +380,236 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const { run } = detail;
return (
<div className="standard-workspace polygon-run-workspace">
<PolygonLivePanel />
<section className="workspace-lead workspace-lead--compact">
<section className="polygon-review-lead">
<div>
<span className="section-eyebrow">ПОЛИГОН / КВАЛИФИКАЦИОННЫЙ ПРОГОН</span>
<h2>{run.runId}</h2>
<span className="section-eyebrow">ПОЛИГОН / ЗАПИСЬ ДАТАСЕТА</span>
<h2>{qualification ? "GOOSE · Ground segmentation" : run.scenarioGeneration}</h2>
<p>
Канонический архив Mission Core. Lifecycle live-контура отделён от истории;
команд физическим актуаторам и реального управления здесь нет.
{qualification
? `${qualification.frameCount} кадров · Current и Patchwork++ против публичной разметки`
: "Архивный прогон без покадрового perception review."}
</p>
</div>
<div className="polygon-run-lead-status">
<div>
<StatusBadge tone={stateTone(run.state)}>{stateLabels[run.state]}</StatusBadge>
<span>read-only · {run.reproducibilityTier}</span>
{catalog && catalog.items.length > 1 ? (
<select
aria-label="Выбрать прогон"
value={run.runId}
onChange={(event) => setSelectedRunId(event.target.value)}
>
{catalog.items.map((item) => (
<option key={item.runId} value={item.runId}>{item.runId}</option>
))}
</select>
) : null}
</div>
</section>
<section className="polygon-run-metrics" aria-label="Сводка прогона">
<div>
<span>Состояние</span>
<strong>{stateLabels[run.state]}</strong>
<small>{run.terminalReason ?? "терминальная причина отсутствует"}</small>
</div>
<div>
<span>Провайдеры</span>
<strong>{run.providers.length}</strong>
<small>{run.providerIds.join(" · ")}</small>
</div>
<div>
<span>События</span>
<strong>{detail.eventsTotal}</strong>
<small>{detail.eventsTruncated ? "показан последний фрагмент" : "журнал целиком"}</small>
</div>
<div>
<span>Команды</span>
<strong>{detail.commandCount}</strong>
<small>содержимое не публикуется UI-0</small>
</div>
</section>
{qualification ? (
<section className="polygon-qualification" aria-label="Квалификация ground provider">
<header className="polygon-qualification__heading">
<div>
<span className="section-eyebrow">GOOSE VALIDATION · {qualification.frameCount} КАДРОВ</span>
<h3>Current против Patchwork++</h3>
<p>
Публичная разметка проверяет переносимость ground pipeline. Результат остаётся
shadow-only и не даёт права на навигацию или safety.
</p>
</div>
<StatusBadge tone={qualification.decision.passed ? "success" : "danger"}>
{qualification.decision.passed ? "Shadow candidate" : "Gate не пройден"}
</StatusBadge>
</header>
<div className="polygon-qualification__providers">
{([
["Current", qualification.current],
["Patchwork++", qualification.patchwork],
] as const).map(([label, aggregate]) => (
<article key={label}>
<strong>{label}</strong>
<dl>
<div><dt>Ground IoU</dt><dd>{formatPercent(aggregate.micro.groundIou)}</dd></div>
<div><dt>Natural ground</dt><dd>{formatPercent(aggregate.micro.naturalGroundRecall)}</dd></div>
<div><dt>Obstacle recall</dt><dd>{formatPercent(aggregate.micro.obstacleNonGroundRecall)}</dd></div>
<div><dt>Latency p95</dt><dd>{formatMilliseconds(aggregate.latencyMs.p95)}</dd></div>
</dl>
</article>
))}
</div>
<div className="polygon-qualification__grid">
<article className="polygon-qualification__checks">
<span className="section-eyebrow">ЗАРАНЕЕ ЗАФИКСИРОВАННЫЕ GATES</span>
{qualification && review ? (
<>
<section className="polygon-review-player" aria-label="Покадровый просмотр GOOSE">
<header className="polygon-review-player__header">
<div>
<span className="section-eyebrow">КАДР {selectedSummary
? selectedSummary.sequence + 1
: "—"} / {review.frameCount}</span>
<strong>{selectedSummary?.frameId ?? "Загружаем запись"}</strong>
</div>
<div className="polygon-review-mode" aria-label="Режим визуализации">
{viewModes.map(([mode, label]) => (
<button
type="button"
key={mode}
data-active={reviewMode === mode ? "true" : undefined}
onClick={() => setReviewMode(mode)}
>
{label}
</button>
))}
</div>
</header>
<div className="polygon-review-stage">
{scene ? (
<LidarGroundPointCloud frame={scene} mode={reviewMode} />
) : (
<div className="polygon-review-stage__loading">Загружаем кадр</div>
)}
<div className="polygon-review-stage__legend">
<strong>{viewModes.find(([mode]) => mode === reviewMode)?.[1]}</strong>
<span>{modeExplanation(reviewMode)}</span>
</div>
</div>
{selectedSummary ? (
<div className="polygon-review-frame-metrics">
<article>
<span>Current · Ground IoU</span>
<strong>{formatPercent(selectedSummary.current.groundIou)}</strong>
<small>прогноз против эталона</small>
</article>
<article>
<span>Patchwork++ · Ground IoU</span>
<strong>{formatPercent(selectedSummary.patchwork.groundIou)}</strong>
<small>прогноз против эталона</small>
</article>
<article data-positive={selectedSummary.groundIouDelta >= 0 ? "true" : "false"}>
<span>Разница на этом кадре</span>
<strong>{formatDelta(selectedSummary.groundIouDelta)}</strong>
<small>Patchwork++ минус Current</small>
</article>
<article>
<span>Natural ground · PW++</span>
<strong>{formatPercent(
selectedSummary.patchwork.naturalGroundRecall,
)}</strong>
<small>recall на естественном грунте</small>
</article>
</div>
) : null}
<div className="polygon-review-controls">
<button
type="button"
aria-label={playing ? "Пауза" : "Воспроизвести"}
onClick={() => setPlaying((value) => !value)}
>
{playing ? "Пауза" : "Play"}
</button>
<button
type="button"
aria-label="Предыдущий кадр"
disabled={selectedPosition <= 0}
onClick={() => selectPosition(selectedPosition - 1)}
>
</button>
<button
type="button"
aria-label="Следующий кадр"
disabled={selectedPosition >= orderedFrames.length - 1}
onClick={() => selectPosition(selectedPosition + 1)}
>
</button>
<input
type="range"
min={0}
max={Math.max(0, orderedFrames.length - 1)}
value={selectedPosition}
aria-label="Позиция в записи"
onChange={(event) => selectPosition(Number(event.target.value))}
/>
<span>{selectedPosition + 1} / {orderedFrames.length}</span>
</div>
<div className="polygon-review-order">
<div>
{orderLabels.map(([order, label]) => (
<button
type="button"
key={order}
data-active={reviewOrder === order ? "true" : undefined}
onClick={() => changeOrder(order)}
>
{label}
</button>
))}
</div>
<div>
<button
type="button"
onClick={() => bestFrame && setSelectedFrameId(bestFrame.frameId)}
>
Лучший прирост
</button>
<button
type="button"
onClick={() => worstFrame && setSelectedFrameId(worstFrame.frameId)}
>
Худшее ухудшение
</button>
</div>
</div>
<div
className="polygon-review-timeline"
aria-label={`Все ${orderedFrames.length} кадров`}
>
{orderedFrames.map((frame, index) => (
<button
type="button"
key={frame.frameId}
aria-label={`Кадр ${frame.sequence + 1}: ${formatDelta(frame.groundIouDelta)}`}
title={`${frame.frameId} · ${formatDelta(frame.groundIouDelta)}`}
data-active={frame.frameId === selectedFrameId ? "true" : undefined}
style={{ "--frame-color": frameColor(frame.groundIouDelta) } as CSSProperties}
onClick={() => selectPosition(index)}
/>
))}
</div>
<p className="polygon-review-timeline-note">
Каждая риска доступный кадр. Светлая означает улучшение IoU, красная
ухудшение. Можно нажать на любую и проверить результат вручную.
</p>
</section>
<section className="polygon-review-summary">
<header>
<div>
<span className="section-eyebrow">ВЕСЬ VALIDATION SPLIT</span>
<h3>Что именно сравнивалось</h3>
</div>
<StatusBadge tone={qualification.decision.passed ? "success" : "danger"}>
{qualification.decision.passed ? "27 / 27 проверок" : "Есть проваленные проверки"}
</StatusBadge>
</header>
<p>
На каждом из {qualification.frameCount} нативных LiDAR-кадров оба алгоритма
независимо решали одну задачу: какие точки являются землёй. Их ответы
сравнивались с публичной point-aligned разметкой GOOSE.
</p>
<div>
<article>
<span>Ground IoU</span>
<strong>{formatPercent(qualification.current.micro.groundIou)}</strong>
<i></i>
<strong>{formatPercent(qualification.patchwork.micro.groundIou)}</strong>
<small>{formatDelta(
qualification.patchwork.micro.groundIou
- qualification.current.micro.groundIou,
)}</small>
</article>
<article>
<span>Natural ground recall</span>
<strong>{formatPercent(
qualification.current.micro.naturalGroundRecall,
)}</strong>
<i></i>
<strong>{formatPercent(
qualification.patchwork.micro.naturalGroundRecall,
)}</strong>
<small>{formatDelta(
qualification.patchwork.micro.naturalGroundRecall
- qualification.current.micro.naturalGroundRecall,
)}</small>
</article>
<article>
<span>Latency p95</span>
<strong>{formatMilliseconds(qualification.current.latencyMs.p95)}</strong>
<i></i>
<strong>{formatMilliseconds(qualification.patchwork.latencyMs.p95)}</strong>
<small>на кадр</small>
</article>
</div>
</section>
<details className="polygon-review-analysis">
<summary>Полные метрики и деградации</summary>
<div className="polygon-review-analysis__content">
<section>
<span className="section-eyebrow">ACCEPTANCE GATES</span>
{qualification.checks.map((check) => (
<p key={check.checkId} data-passed={check.passed ? "true" : "false"}>
<i aria-hidden="true" />
@@ -335,12 +621,9 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
</code>
</p>
))}
</div>
</article>
<article className="polygon-qualification__degradations">
<span className="section-eyebrow">ДЕТЕРМИНИРОВАННЫЕ ДЕГРАДАЦИИ</span>
<div>
</section>
<section>
<span className="section-eyebrow">ДЕГРАДАЦИИ</span>
{Object.entries(qualification.degradations).map(([profileId, aggregate]) => (
<p key={profileId}>
<strong>{profileId}</strong>
@@ -349,194 +632,59 @@ export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
<span>p95 {formatMilliseconds(aggregate.latencyMs.p95)}</span>
</p>
))}
</div>
</article>
</div>
<div className="polygon-qualification__failures">
<article>
<span className="section-eyebrow">ХУДШИЕ КАДРЫ</span>
<div>
{qualification.worstFrames.slice(0, 5).map((frame) => (
<button
type="button"
key={frame.frameId}
data-active={frame.frameId === selectedFailureFrameId ? "true" : undefined}
onClick={() => setSelectedFailureFrameId(frame.frameId)}
>
<span>{frame.frameId}</span>
<code>
PW {formatPercent(frame.patchworkGroundIou)} ·
Δ {formatPercent(frame.groundIouDelta)}
</code>
</button>
))}
</div>
</article>
<article className="polygon-qualification__viewer">
<header>
<div>
<span className="section-eyebrow">РАЗБОР ОШИБКИ</span>
<strong>{failurePreview?.frameId ?? "Загружаем кадр"}</strong>
</div>
<div className="lidar-ground-modes" aria-label="Режим отображения ошибки">
{([
["ground-truth", "Ground truth"],
["current", "Current"],
["candidate", "Patchwork++"],
["disagreement", "Ошибка Current"],
["candidate-disagreement", "Ошибка PW++"],
] as const).map(([mode, label]) => (
<button
type="button"
key={mode}
data-active={failureMode === mode ? "true" : undefined}
onClick={() => setFailureMode(mode)}
>
{label}
</button>
))}
</div>
</header>
{failureFrame ? (
<LidarGroundPointCloud frame={failureFrame} mode={failureMode} />
) : (
<div className="polygon-qualification__viewer-empty">
Читаем bounded preview из sealed-артефакта прогона
</div>
)}
</article>
</div>
</section>
) : qualificationError ? (
<div className="polygon-qualification__error">
Квалификационный отчёт недоступен: {qualificationError}
</div>
) : null}
<div className="polygon-run-layout">
<GlassSurface className="polygon-run-catalog" padding="lg">
<header className="polygon-run-panel-heading">
<div>
<span className="section-eyebrow">ПОСЛЕДНИЕ ПРОГОНЫ</span>
<h3>{catalog?.total ?? 0} в источнике</h3>
</section>
</div>
<Button
size="compact"
variant="secondary"
onClick={() => setReloadGeneration((value) => value + 1)}
>
Обновить
</Button>
</header>
<div className="polygon-run-list">
{catalog?.items.map((item) => (
<button
type="button"
key={item.runId}
data-active={item.runId === run.runId ? "true" : undefined}
onClick={() => setSelectedRunId(item.runId)}
>
<i data-state={item.state} aria-hidden="true" />
<span>
<strong>{item.runId}</strong>
<small>{formatTimestamp(item.createdAtUtc)}</small>
</span>
<em>{stateLabels[item.state]}</em>
</button>
))}
</div>
</GlassSurface>
</details>
</>
) : qualification ? (
<section className="polygon-review-unavailable">
<span className="section-eyebrow">ПОКАДРОВЫЙ ПРОСМОТР</span>
<h3>Review-pack ещё не опубликован</h3>
<p>
Числовой отчёт есть, но мы не показываем его как достаточный результат:
оператор должен иметь возможность проверить все {qualification.frameCount} кадров.
</p>
{reviewError ? <small>{reviewError}</small> : null}
</section>
) : (
<section className="polygon-review-unavailable">
<span className="section-eyebrow">АРХИВНЫЙ ПРОГОН</span>
<h3>В этой записи нет perception review</h3>
<p>Технические доказательства доступны в раскрытии ниже.</p>
</section>
)}
<GlassSurface className="polygon-run-identity" padding="lg">
<span className="section-eyebrow">ИДЕНТИЧНОСТЬ И ГРАНИЦА</span>
<details className="polygon-run-technical">
<summary>Технические детали прогона</summary>
<div className="polygon-run-technical__content">
<dl>
<div><dt>Run ID</dt><dd>{run.runId}</dd></div>
<div><dt>Сценарий</dt><dd>{run.scenarioGeneration}</dd></div>
<div><dt>Профиль</dt><dd>{run.profileGeneration}</dd></div>
<div><dt>Host profile</dt><dd>{run.hostProfileId}</dd></div>
<div><dt>Mission Core</dt><dd><code>{run.missionCoreCommit.slice(0, 12)}</code></dd></div>
<div><dt>Clock</dt><dd><code>{run.clockDomain}</code></dd></div>
<div><dt>Seed</dt><dd>{run.seed}</dd></div>
<div><dt>Mission Core</dt><dd>{run.missionCoreCommit.slice(0, 12)}</dd></div>
<div><dt>Начало</dt><dd>{formatTimestamp(run.startedAtUtc)}</dd></div>
<div><dt>Завершение</dt><dd>{formatTimestamp(run.endedAtUtc)}</dd></div>
</dl>
<div className="polygon-run-safety-boundary">
<StatusBadge tone="success">Virtual only</StatusBadge>
<p>
Actuator authority: {run.authority.actuatorAuthority ? "да" : "нет"} ·
direct setpoints: {run.authority.directActuatorSetpointsAllowed ? "да" : "нет"} ·
navigation/safety accepted: {run.authority.navigationOrSafetyAccepted ? "да" : "нет"}
</p>
</div>
</GlassSurface>
</div>
<section className="polygon-run-providers" aria-label="Провайдеры прогона">
{run.providers.map((provider) => (
<div key={provider.identifier}>
<i aria-hidden="true" />
<span>
<strong>{provider.identifier}</strong>
<small>{provider.version}</small>
</span>
<code>{provider.revision.slice(0, 16)}</code>
</div>
))}
</section>
<GlassSurface className="polygon-run-events" padding="lg">
<header className="polygon-run-panel-heading">
<div>
<span className="section-eyebrow">ЖУРНАЛ СОБЫТИЙ</span>
<h3>Последние переходы и факты</h3>
</div>
<span>revision {run.revision}</span>
</header>
<div className="polygon-run-event-list">
{visibleEvents.map((event) => (
<article key={event.sequence}>
<span className="polygon-run-event-sequence">
{String(event.sequence).padStart(3, "0")}
</span>
<div>
<strong>{event.eventType}</strong>
<small>{formatTimestamp(event.observedAtUtc)}</small>
<code>{JSON.stringify(event.payload)}</code>
</div>
<span>{event.simTimeNs === null ? "host" : `${event.simTimeNs} ns`}</span>
</article>
))}
<section>
<h4>Провайдеры</h4>
{run.providers.map((provider) => (
<p key={provider.identifier}>
<strong>{provider.identifier}</strong>
<span>{provider.version} · {provider.revision.slice(0, 16)}</span>
</p>
))}
</section>
<section>
<h4>Артефакты</h4>
{detail.artifacts.map((artifact) => (
<p key={artifact.artifactId}>
<strong>{artifact.kind}</strong>
<span>{formatBytes(artifact.byteLength)} · {artifact.sha256.slice(0, 12)}</span>
</p>
))}
</section>
</div>
</GlassSurface>
<div className="polygon-run-evidence-grid">
<GlassSurface className="polygon-run-artifacts" padding="lg">
<span className="section-eyebrow">АРТЕФАКТЫ</span>
<h3>{detail.artifacts.length || run.artifactCount} ссылок в индексе</h3>
{detail.artifacts.length ? (
<div>
{detail.artifacts.map((artifact) => (
<article key={artifact.artifactId}>
<span>
<strong>{artifact.kind}</strong>
<small>{artifact.relativePath}</small>
</span>
<code>{artifact.sha256.slice(0, 12)} · {formatBytes(artifact.byteLength)}</code>
</article>
))}
</div>
) : (
<p>В журнале прогона нет зарегистрированных artifact-index записей.</p>
)}
</GlassSurface>
<GlassSurface className="polygon-run-limitations" padding="lg">
<span className="section-eyebrow">ЧЕСТНАЯ ГРАНИЦА UI-0</span>
<h3>Что этот результат ещё не доказывает</h3>
<ul>
{detail.limitations.map((limitation) => <li key={limitation}>{limitation}</li>)}
</ul>
</GlassSurface>
</div>
</details>
</div>
);
}
@@ -6,6 +6,8 @@ import { createServer } from "vite";
let server;
let decodeGroundQualification;
let decodeGroundFailurePreview;
let decodeGroundReview;
let decodeGroundReviewFrame;
let fetchGroundQualification;
let GroundQualificationContractError;
@@ -70,6 +72,8 @@ before(async () => {
({
decodeGroundQualification,
decodeGroundFailurePreview,
decodeGroundReview,
decodeGroundReviewFrame,
fetchGroundQualification,
GroundQualificationContractError,
} = await server.ssrLoadModule("/src/core/polygon/groundQualification.ts"));
@@ -132,3 +136,61 @@ test("treats missing qualification as a generic run", async () => {
});
assert.equal(result, null);
});
test("decodes a complete ordered frame review", () => {
const decoded = decodeGroundReview({
schema_version: "missioncore.polygon-ground-review/v1",
access: "read-only",
run_id: "goose-ground-abc",
identity_sha256: "b".repeat(64),
source_id: "goose-3d/v2025-08-22",
frame_count: 2,
preview_points: 12_000,
frames: [0, 1].map((sequence) => ({
sequence,
frame_id: `2022-07-22_flight__0071_000${sequence}`,
source_point_count: 100_000,
point_count: 12_000,
current: {
ground_iou: 0.5,
natural_ground_recall: 0.55,
obstacle_non_ground_recall: 0.91,
latency_ms: 3_000,
},
patchworkpp: {
ground_iou: 0.7,
natural_ground_recall: 0.75,
obstacle_non_ground_recall: 0.94,
latency_ms: 20,
},
ground_iou_delta: 0.2,
})),
});
assert.equal(decoded.frames.length, 2);
assert.equal(decoded.frames[1].sequence, 1);
assert.equal(decoded.frames[0].patchwork.groundIou, 0.7);
});
test("decodes a point-aligned review frame with intensity", () => {
const decoded = decodeGroundReviewFrame({
schema_version: "missioncore.polygon-ground-review-frame/v1",
access: "read-only",
run_id: "goose-ground-abc",
source_id: "goose-3d/v2025-08-22",
frame_id: "2022-07-22_flight__0071_0001",
source_point_count: 2,
point_count: 2,
sampling: "deterministic-even-index",
points_xyz_m: [[0, 0, 0], [1, 1, 1]],
intensity_0_255: [12, 255],
ground_truth_ground: [1, 0],
evaluated: [1, 1],
current_ground: [0, 0],
patchwork_ground: [1, 0],
current_disagreement: [1, 0],
patchwork_disagreement: [0, 0],
safety: { navigation_or_safety_accepted: false },
});
assert.deepEqual(decoded.intensity0To255, [12, 255]);
assert.equal(decoded.frameId, "2022-07-22_flight__0071_0001");
});