feat(lab): verify and expose real evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 15:36:48 +03:00
parent 453d760be4
commit dc55ff16c9
9 changed files with 2007 additions and 47 deletions
@@ -0,0 +1,540 @@
export interface E29ReviewFrame {
frameIndex: number;
sourceFrameIndex: number;
sessionSeconds: number;
conflictCount: number;
semanticObservationCount: number;
geometryOnlyClusterCount: number;
}
export interface E29EvidenceResult {
resultId: string;
createdAtUtc: string | null;
status: "diagnostic-replay-complete";
identity: {
frameCount: number;
timelineStartSeconds: number;
timelineEndSeconds: number;
};
metrics: {
frames: {
total: number;
sourceAvailable: number;
localSurfaceValid: number;
};
semanticObservations: {
total: number;
current: number;
agreementFractionOfCurrent: number;
geometryStatus: {
agree: number;
cameraOnly: number;
conflict: number;
unknown: number;
};
};
geometryOnlyOccupied: {
clusterCount: number;
pointCount: number;
};
runtime: {
buildElapsedMs: number;
frameProcessingP95Ms: number;
};
};
decision: {
cameraFirstContractImplemented: boolean;
parallelGeometryOnlyLayerImplemented: boolean;
productionPromotion: boolean;
nextGate: string;
};
limitations: readonly string[];
authority: {
commandsEnabled: false;
navigationOrSafetyAccepted: false;
};
reviewFrames: readonly E29ReviewFrame[];
linkedEvidence: {
sourceSessionId: string;
localSurfaceModelId: string;
sourcePackId: string;
sourceResultId: string;
publishedOnControlPlane: true;
workerCopyRequired: false;
};
groundTruth: false;
}
export interface E29EvidenceCatalog {
configured: boolean;
candidateTotal: number;
invalidTotal: number;
items: readonly E29EvidenceResult[];
}
export interface E29SemanticObservation {
trackId: number;
label: string;
geometryStatus: string;
geometryReason: string;
rangeM: number | null;
score: number;
support: {
classifiedPoints: number;
occupiedPoints: number;
surfacePoints: number;
};
}
export interface E29GeometryOnlyCluster {
nearestRangeM: number;
pointCount: number;
voxelCount: number;
heightRangeM: readonly [number, number];
}
export interface E29EvidenceFrame {
resultId: string;
frameIndex: number;
sourceFrameIndex: number;
sessionSeconds: number;
semanticObservations: readonly E29SemanticObservation[];
geometryOnlyOccupied: readonly E29GeometryOnlyCluster[];
}
export class E29EvidenceContractError extends Error {
constructor(message: string) {
super(message);
this.name = "E29EvidenceContractError";
}
}
type E29Fetch = (
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 E29EvidenceContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new E29EvidenceContractError(`${label}: ожидалась строка.`);
}
return value;
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new E29EvidenceContractError(`${label}: ожидалось число.`);
}
return value;
}
function integerValue(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new E29EvidenceContractError(`${label}: ожидалось целое число.`);
}
return parsed;
}
function booleanValue(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new E29EvidenceContractError(`${label}: ожидался boolean.`);
}
return value;
}
function falseValue(value: unknown, label: string): false {
if (booleanValue(value, label) !== false) {
throw new E29EvidenceContractError(`${label}: ожидалось false.`);
}
return false;
}
function trueValue(value: unknown, label: string): true {
if (booleanValue(value, label) !== true) {
throw new E29EvidenceContractError(`${label}: ожидалось true.`);
}
return true;
}
function nullableNumber(value: unknown, label: string): number | null {
return value === null ? null : numberValue(value, label);
}
function tuple2(value: unknown, label: string): readonly [number, number] {
if (!Array.isArray(value) || value.length !== 2) {
throw new E29EvidenceContractError(`${label}: ожидалась пара чисел.`);
}
return [
numberValue(value[0], `${label}[0]`),
numberValue(value[1], `${label}[1]`),
];
}
function parseReviewFrame(value: unknown): E29ReviewFrame {
const source = record(value, "review_frame");
return {
frameIndex: integerValue(source.frame_index, "review_frame.frame_index"),
sourceFrameIndex: integerValue(
source.source_frame_index,
"review_frame.source_frame_index",
),
sessionSeconds: numberValue(
source.session_seconds,
"review_frame.session_seconds",
),
conflictCount: integerValue(
source.conflict_count,
"review_frame.conflict_count",
),
semanticObservationCount: integerValue(
source.semantic_observation_count,
"review_frame.semantic_observation_count",
),
geometryOnlyClusterCount: integerValue(
source.geometry_only_cluster_count,
"review_frame.geometry_only_cluster_count",
),
};
}
function parseEvidenceResult(value: unknown): E29EvidenceResult {
const source = record(value, "E29 result");
const identity = record(source.identity, "E29 identity");
const metrics = record(source.metrics, "E29 metrics");
const frames = record(metrics.frames, "E29 metrics.frames");
const semantic = record(
metrics.semantic_observations,
"E29 metrics.semantic_observations",
);
const geometryStatus = record(
semantic.geometry_status,
"E29 metrics.semantic_observations.geometry_status",
);
const geometryOnly = record(
metrics.geometry_only_occupied,
"E29 metrics.geometry_only_occupied",
);
const runtime = record(metrics.runtime, "E29 metrics.runtime");
const frameProcessing = record(
runtime.frame_processing_ms,
"E29 metrics.runtime.frame_processing_ms",
);
const decision = record(source.decision, "E29 decision");
const authority = record(source.authority, "E29 authority");
const linked = record(source.linked_evidence, "E29 linked evidence");
const status = stringValue(source.status, "E29 status");
if (status !== "diagnostic-replay-complete") {
throw new E29EvidenceContractError("E29 status не завершён.");
}
if (!Array.isArray(source.limitations) || !Array.isArray(source.review_frames)) {
throw new E29EvidenceContractError("E29 collections имеют неверный формат.");
}
return {
resultId: stringValue(source.result_id, "E29 result_id"),
createdAtUtc: source.created_at_utc === null
? null
: stringValue(source.created_at_utc, "E29 created_at_utc"),
status,
identity: {
frameCount: integerValue(identity.frame_count, "E29 identity.frame_count"),
timelineStartSeconds: numberValue(
identity.timeline_start_seconds,
"E29 identity.timeline_start_seconds",
),
timelineEndSeconds: numberValue(
identity.timeline_end_seconds,
"E29 identity.timeline_end_seconds",
),
},
metrics: {
frames: {
total: integerValue(frames.total, "E29 metrics.frames.total"),
sourceAvailable: integerValue(
frames.source_available,
"E29 metrics.frames.source_available",
),
localSurfaceValid: integerValue(
frames.local_surface_valid,
"E29 metrics.frames.local_surface_valid",
),
},
semanticObservations: {
total: integerValue(
semantic.total,
"E29 metrics.semantic_observations.total",
),
current: integerValue(
semantic.current,
"E29 metrics.semantic_observations.current",
),
agreementFractionOfCurrent: numberValue(
semantic.agreement_fraction_of_current,
"E29 metrics.semantic_observations.agreement_fraction_of_current",
),
geometryStatus: {
agree: integerValue(geometryStatus.agree, "E29 geometry_status.agree"),
cameraOnly: integerValue(
geometryStatus["single-source-camera"],
"E29 geometry_status.single-source-camera",
),
conflict: integerValue(
geometryStatus.conflict,
"E29 geometry_status.conflict",
),
unknown: integerValue(
geometryStatus.unknown,
"E29 geometry_status.unknown",
),
},
},
geometryOnlyOccupied: {
clusterCount: integerValue(
geometryOnly.cluster_count,
"E29 geometry_only_occupied.cluster_count",
),
pointCount: integerValue(
geometryOnly.point_count,
"E29 geometry_only_occupied.point_count",
),
},
runtime: {
buildElapsedMs: numberValue(
runtime.build_elapsed_ms,
"E29 runtime.build_elapsed_ms",
),
frameProcessingP95Ms: numberValue(
frameProcessing.p95,
"E29 runtime.frame_processing_ms.p95",
),
},
},
decision: {
cameraFirstContractImplemented: booleanValue(
decision.camera_first_contract_implemented,
"E29 decision.camera_first_contract_implemented",
),
parallelGeometryOnlyLayerImplemented: booleanValue(
decision.parallel_geometry_only_layer_implemented,
"E29 decision.parallel_geometry_only_layer_implemented",
),
productionPromotion: booleanValue(
decision.production_promotion,
"E29 decision.production_promotion",
),
nextGate: stringValue(decision.next_gate, "E29 decision.next_gate"),
},
limitations: source.limitations.map((item, index) => (
stringValue(item, `E29 limitations[${index}]`)
)),
authority: {
commandsEnabled: falseValue(
authority.commands_enabled,
"E29 authority.commands_enabled",
),
navigationOrSafetyAccepted: falseValue(
authority.navigation_or_safety_accepted,
"E29 authority.navigation_or_safety_accepted",
),
},
reviewFrames: source.review_frames.map(parseReviewFrame),
linkedEvidence: {
sourceSessionId: stringValue(
linked.source_session_id,
"E29 linked_evidence.source_session_id",
),
localSurfaceModelId: stringValue(
linked.local_surface_model_id,
"E29 linked_evidence.local_surface_model_id",
),
sourcePackId: stringValue(
linked.source_pack_id,
"E29 linked_evidence.source_pack_id",
),
sourceResultId: stringValue(
linked.source_result_id,
"E29 linked_evidence.source_result_id",
),
publishedOnControlPlane: trueValue(
linked.published_on_control_plane,
"E29 linked_evidence.published_on_control_plane",
),
workerCopyRequired: falseValue(
linked.worker_copy_required,
"E29 linked_evidence.worker_copy_required",
),
},
groundTruth: falseValue(source.ground_truth, "E29 ground_truth"),
};
}
export function parseE29EvidenceCatalog(value: unknown): E29EvidenceCatalog {
const source = record(value, "E29 catalog");
if (source.schema_version !== "missioncore.laboratory-e29-catalog/v1") {
throw new E29EvidenceContractError("E29 catalog schema не поддерживается.");
}
if (!Array.isArray(source.items)) {
throw new E29EvidenceContractError("E29 catalog.items имеет неверный формат.");
}
return {
configured: booleanValue(source.configured, "E29 catalog.configured"),
candidateTotal: integerValue(
source.candidate_total,
"E29 catalog.candidate_total",
),
invalidTotal: integerValue(
source.invalid_total,
"E29 catalog.invalid_total",
),
items: source.items.map(parseEvidenceResult),
};
}
function parseSemanticObservation(value: unknown): E29SemanticObservation {
const source = record(value, "E29 semantic observation");
const support = record(source.support, "E29 semantic observation.support");
return {
trackId: integerValue(source.track_id, "E29 semantic observation.track_id"),
label: stringValue(source.label, "E29 semantic observation.label"),
geometryStatus: stringValue(
source.geometry_status,
"E29 semantic observation.geometry_status",
),
geometryReason: stringValue(
source.geometry_reason,
"E29 semantic observation.geometry_reason",
),
rangeM: nullableNumber(source.range_m, "E29 semantic observation.range_m"),
score: numberValue(source.score, "E29 semantic observation.score"),
support: {
classifiedPoints: integerValue(
support.classified_points_in_bbox,
"E29 support.classified_points_in_bbox",
),
occupiedPoints: integerValue(
support.occupied_points_in_bbox,
"E29 support.occupied_points_in_bbox",
),
surfacePoints: integerValue(
support.surface_points_in_bbox,
"E29 support.surface_points_in_bbox",
),
},
};
}
function parseGeometryOnly(value: unknown): E29GeometryOnlyCluster {
const source = record(value, "E29 geometry-only cluster");
return {
nearestRangeM: numberValue(
source.nearest_range_m,
"E29 geometry-only.nearest_range_m",
),
pointCount: integerValue(
source.point_count,
"E29 geometry-only.point_count",
),
voxelCount: integerValue(
source.voxel_count,
"E29 geometry-only.voxel_count",
),
heightRangeM: tuple2(
source.height_range_m,
"E29 geometry-only.height_range_m",
),
};
}
export function parseE29EvidenceFrame(value: unknown): E29EvidenceFrame {
const source = record(value, "E29 frame response");
if (source.schema_version !== "missioncore.laboratory-e29-frame/v1") {
throw new E29EvidenceContractError("E29 frame schema не поддерживается.");
}
const frame = record(source.frame, "E29 frame");
if (
frame.schema_version !== "missioncore.e29-camera-geometry-frame/v1"
|| !Array.isArray(frame.semantic_observations)
|| !Array.isArray(frame.geometry_only_occupied)
) {
throw new E29EvidenceContractError("E29 frame имеет неверный формат.");
}
return {
resultId: stringValue(source.result_id, "E29 frame result_id"),
frameIndex: integerValue(frame.frame_index, "E29 frame.frame_index"),
sourceFrameIndex: integerValue(
frame.source_frame_index,
"E29 frame.source_frame_index",
),
sessionSeconds: numberValue(
frame.session_seconds,
"E29 frame.session_seconds",
),
semanticObservations: frame.semantic_observations.map(
parseSemanticObservation,
),
geometryOnlyOccupied: frame.geometry_only_occupied.map(parseGeometryOnly),
};
}
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 E29EvidenceContractError(detail);
}
return payload;
}
export async function fetchE29EvidenceCatalog(
options: { signal?: AbortSignal; fetcher?: E29Fetch } = {},
): Promise<E29EvidenceCatalog> {
const fetcher = options.fetcher ?? fetch;
const response = await fetcher("/api/v1/laboratory/e29/results?limit=1", {
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
});
return parseE29EvidenceCatalog(
await responseJson(response, "Не удалось получить LAB E29."),
);
}
export async function fetchE29EvidenceFrame(
resultId: string,
frameIndex: number,
options: { signal?: AbortSignal; fetcher?: E29Fetch } = {},
): Promise<E29EvidenceFrame> {
if (
!/^e29-camera-geometry-[a-f0-9]{64}$/.test(resultId)
|| !Number.isSafeInteger(frameIndex)
|| frameIndex < 0
) {
throw new E29EvidenceContractError("Некорректный LAB E29 frame id.");
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/laboratory/e29/results/${resultId}/frames/${frameIndex}`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseE29EvidenceFrame(
await responseJson(response, "Не удалось получить кадр LAB E29."),
);
}
@@ -1488,7 +1488,7 @@ export async function fetchLidarLocalSurfaces(
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
): Promise<LidarLocalSurfaceCatalog> {
const fetcher = options.fetcher ?? fetch;
const response = await fetcher("/api/v1/lidar/local-surfaces?limit=10", {
const response = await fetcher("/api/v1/lidar/local-surfaces?limit=1", {
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
@@ -2592,6 +2592,111 @@
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);
@@ -2601,6 +2706,14 @@
.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 {
@@ -44,6 +44,16 @@ import type {
MissionRuntimeState,
ObservationSourceDescriptor,
} from "../core/runtime/contracts";
import {
fetchE29EvidenceCatalog,
fetchE29EvidenceFrame,
type E29EvidenceFrame,
type E29EvidenceResult,
} from "../core/laboratory/e29Evidence";
import {
fetchLidarLocalSurfaces,
type LidarLocalSurfaceModel,
} from "../core/lidar/localSurface";
import {
RerunViewport,
isRecordedPlaybackPresentationReady,
@@ -1271,39 +1281,200 @@ function LaboratoryTask({
);
}
function E29LaboratoryResult({ rigLabel }: { rigLabel: string }) {
function formatSeconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`;
}
function E29LaboratoryResult({
props,
rigLabel,
result,
sourceSession,
loading,
error,
}: {
props: WorkspaceRendererProps;
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 (
<>
<LaboratoryTask
title="LAB E29 · camera-first semantics + независимая геометрия"
description="Проверяется рабочая продуктовая граница: камера сохраняет семантический класс и идентичность объекта, LiDAR подтверждает метрическую дальность и занятую геометрию по локальной поверхности L2.6. Отсутствие точек не объявляется свободным пространством."
status="Диагностический replay"
status="Проверенные артефакты"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · камера + LiDAR · worker D` },
{ label: "Источник", value: "RAVNOVES00 · 4 489 кадров" },
{ label: "Объектов", value: "20 513 наблюдений" },
{ label: "Полномочия", value: "Только shadow · команды запрещены" },
{
label: "Источник",
value: `${sourceSession.label} · ${formatNumber(result.identity.frameCount, 0)} кадров`,
},
{
label: "Наблюдений",
value: formatNumber(semantic.total, 0),
},
{
label: "Артефакты",
value: "Control plane · read-only · hash verified",
},
]}
/>
<section className="lab-result-surface">
<header>
<span className="section-eyebrow">ИСХОДНЫЕ ДАННЫЕ</span>
<strong>LiDAR, траектория и камера RAVNOVES00</strong>
</header>
{replayReady ? (
<SpatialWorkspace {...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>
)}
</section>
<section className="laboratory-result-summary">
<header>
<div>
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
<h2>Архитектурная схема подтверждена, safety-gate не пройден</h2>
<h2>Camera-first контракт рассчитан, production gate не пройден</h2>
</div>
<StatusBadge tone="success">Replay завершён</StatusBadge>
<StatusBadge tone={result.decision.productionPromotion ? "success" : "warning"}>
{result.decision.productionPromotion ? "Допущено" : "Только диагностика"}
</StatusBadge>
</header>
<div className="laboratory-result-metrics">
<div><span>Поддержка геометрией</span><strong>6 341</strong><small>32,31% current observations</small></div>
<div><span>Camera-only</span><strong>13 246</strong><small>Семантика сохранена</small></div>
<div><span>Postprocess p95</span><strong>2,517 мс</strong><small>8,467 с полный build</small></div>
<div><span>Geometry-only</span><strong>21 321</strong><small>Занято / класс неизвестен</small></div>
<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>
Следующий gate покадровый разбор конфликтов, camera-only пробелов и
geometry-only компонентов, затем source-paced shadow с deadline и staleness.
</p>
<p>{result.decision.nextGate}</p>
</section>
<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>
</>
);
@@ -1421,6 +1592,10 @@ function PublishedLaboratoryResult({
function LabArchiveWorkspace(props: WorkspaceRendererProps) {
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 [evidenceLoading, setEvidenceLoading] = useState(true);
const [evidenceError, setEvidenceError] = useState<string | null>(null);
const sessions = useObservationSessions({
limit: 100,
replayEnabled: props.sessionArchive.blockedReason === null,
@@ -1429,9 +1604,47 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
onReplaySettled: props.sessionArchive.onReplaySettled,
});
const publishedWorks = useMemo(
() => sessions.items.filter((session) => session.lab !== null),
() => 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 }),
]).then(([e28, e29]) => {
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;
setE28Model(nextE28);
setE29Result(nextE29);
const failures = [
e28.status === "rejected" ? "E28" : null,
e29.status === "rejected" ? "E29" : 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
@@ -1442,22 +1655,44 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
: "";
return sensorToken ? sensorToken.toLocaleUpperCase("ru-RU") : "Сенсорный риг";
}, [props.deviceLabel, publishedWorks]);
const profiles: readonly LaboratoryOption<LaboratoryProfileId>[] = [
{
id: "sensor-fusion",
label: `${rigLabel} · камера + LiDAR · worker D`,
},
{
id: "published-perception",
label: `${rigLabel} · опубликованный perception pipeline`,
},
];
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",
});
}
return items;
}, [e28Model, e29Result, 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"
? [
{ id: "e28-local-surface", label: "LAB E28 · локальная поверхность L2.6" },
{ id: "e29-camera-geometry", label: "LAB E29 · camera-first + geometry" },
]
? sensorWorks
: publishedWorks.map((session) => ({
id: `session:${session.id}` as const,
label: `${session.lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`,
@@ -1468,18 +1703,49 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
const selectedSession = selectedSessionId
? publishedWorks.find((session) => session.id === selectedSessionId) ?? null
: null;
const e29SourceSession = e29Result
? sourceSessions.get(e29Result.linkedEvidence.sourceSessionId) ?? null
: null;
useEffect(() => {
if (profileId !== "published-perception" || workId.startsWith("session:")) return;
const first = publishedWorks[0];
if (!first) return;
setWorkId(`session:${first.id}`);
}, [profileId, publishedWorks, workId]);
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") {
setWorkId("e28-local-surface");
const first = sensorWorks[0];
if (first) setWorkId(first.id);
return;
}
const first = publishedWorks[0];
@@ -1491,10 +1757,41 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
const selectWork = (next: LaboratoryWorkId) => {
setWorkId(next);
if (!next.startsWith("session:")) return;
void sessions.replay(next.slice("session:".length));
if (next.startsWith("session:")) {
void sessions.replay(next.slice("session:".length));
return;
}
if (next === "e29-camera-geometry" && e29SourceSession) {
void sessions.replay(e29SourceSession.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>
);
}
return (
<div className="lab-archive-workspace">
<LaboratorySelector
@@ -1524,12 +1821,15 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
<LaboratoryTask
title="LAB E28 · локальная модель поверхности L2.6"
description="Полная запись RAVNOVES00 воспроизводится через bounded shadow-контур. Модель оценивает локальную поверхность, наблюдаемые препятствия, временные скачки и ошибки предсказания без изменения исходного сенсора и без командного канала."
status="Диагностический replay"
status="Проверенные артефакты"
facts={[
{ label: "Конфигурация", value: `${rigLabel} · LiDAR + pose · worker D` },
{ label: "Покрытие", value: "3 928 / 3 928 кадров" },
{
label: "Покрытие",
value: `${formatNumber(e28Model?.metrics.frames.valid ?? 0, 0)} / ${formatNumber(e28Model?.metrics.frames.total ?? 0, 0)} кадров`,
},
{ label: "Режим", value: "Recorded-source-paced shadow" },
{ label: "Полномочия", value: "Навигация и команды запрещены" },
{ label: "Артефакты", value: "Control plane · read-only · hash verified" },
]}
/>
<LidarQualityWorkspace
@@ -1538,8 +1838,19 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
onOpenObservation={() => props.navigation.openView("spatial-scene")}
/>
</>
) : workId === "e29-camera-geometry" ? (
<E29LaboratoryResult rigLabel={rigLabel} />
) : 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
}
/>
) : selectedSession ? (
<PublishedLaboratoryResult
props={props}
@@ -1550,8 +1861,8 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
) : (
<div className="laboratory-result-pending">
<Icon name="database" size={20} />
<strong>В этом профиле пока нет опубликованных работ</strong>
<p>Работы появятся после публикации проверенного LAB-provenance.</p>
<strong>Работа не прошла ревизию</strong>
<p>Неподтверждённый результат скрыт из лабораторного каталога.</p>
</div>
)}
</div>
@@ -0,0 +1,218 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let parseE29EvidenceCatalog;
let parseE29EvidenceFrame;
let fetchE29EvidenceCatalog;
let fetchE29EvidenceFrame;
let E29EvidenceContractError;
const resultId = `e29-camera-geometry-${"a".repeat(64)}`;
function result(overrides = {}) {
return {
result_id: resultId,
created_at_utc: "2026-07-26T00:00:00Z",
status: "diagnostic-replay-complete",
identity: {
frame_count: 4489,
timeline_start_seconds: 35.4,
timeline_end_seconds: 484.0,
},
metrics: {
frames: {
total: 4489,
source_available: 3928,
local_surface_valid: 3928,
},
semantic_observations: {
total: 20513,
current: 19625,
agreement_fraction_of_current: 0.3231,
geometry_status: {
agree: 6341,
"single-source-camera": 13246,
conflict: 38,
unknown: 888,
},
},
geometry_only_occupied: {
cluster_count: 21321,
point_count: 2021343,
},
runtime: {
build_elapsed_ms: 8466.6,
frame_processing_ms: { p95: 2.517 },
},
},
decision: {
camera_first_contract_implemented: true,
parallel_geometry_only_layer_implemented: true,
production_promotion: false,
next_gate: "operator review",
},
limitations: ["not ground truth"],
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
},
review_frames: [{
frame_index: 343,
source_frame_index: 343,
session_seconds: 69.799,
conflict_count: 1,
semantic_observation_count: 6,
geometry_only_cluster_count: 5,
}],
linked_evidence: {
source_session_id: "20260720T065719Z_viewer_live",
local_surface_model_id: `k1-local-surface-${"b".repeat(64)}`,
source_pack_id: `e10-lidar-pack-${"c".repeat(64)}`,
source_result_id: `e10-integrated-perception-${"d".repeat(64)}`,
published_on_control_plane: true,
worker_copy_required: false,
},
ground_truth: false,
access: "read-only",
...overrides,
};
}
function catalog(overrides = {}) {
return {
schema_version: "missioncore.laboratory-e29-catalog/v1",
configured: true,
items: [result()],
candidate_total: 3,
invalid_total: 0,
access: "read-only",
...overrides,
};
}
function frame(overrides = {}) {
return {
schema_version: "missioncore.laboratory-e29-frame/v1",
result_id: resultId,
frame: {
schema_version: "missioncore.e29-camera-geometry-frame/v1",
frame_index: 343,
source_frame_index: 343,
session_seconds: 69.799,
semantic_observations: [{
track_id: 240042,
label: "car",
geometry_status: "conflict",
geometry_reason: "camera-object-region-observed-as-local-surface",
range_m: null,
score: 0.76,
support: {
classified_points_in_bbox: 8,
occupied_points_in_bbox: 0,
surface_points_in_bbox: 7,
},
}],
geometry_only_occupied: [{
nearest_range_m: 3.71,
point_count: 7,
voxel_count: 5,
height_range_m: [1.61, 1.86],
}],
},
access: "read-only",
...overrides,
};
}
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
parseE29EvidenceCatalog,
parseE29EvidenceFrame,
fetchE29EvidenceCatalog,
fetchE29EvidenceFrame,
E29EvidenceContractError,
} = await server.ssrLoadModule("/src/core/laboratory/e29Evidence.ts"));
});
after(async () => {
await server?.close();
});
test("decodes only verified non-authoritative E29 evidence", () => {
const decoded = parseE29EvidenceCatalog(catalog());
assert.equal(decoded.items.length, 1);
assert.equal(decoded.candidateTotal, 3);
assert.equal(decoded.items[0].metrics.semanticObservations.geometryStatus.conflict, 38);
assert.equal(decoded.items[0].linkedEvidence.publishedOnControlPlane, true);
const decodedFrame = parseE29EvidenceFrame(frame());
assert.equal(decodedFrame.semanticObservations[0].geometryStatus, "conflict");
assert.equal(decodedFrame.geometryOnlyOccupied[0].pointCount, 7);
});
test("rejects command authority and unverified publication", () => {
assert.throws(
() => parseE29EvidenceCatalog(catalog({
items: [result({
authority: {
commands_enabled: true,
navigation_or_safety_accepted: false,
},
})],
})),
E29EvidenceContractError,
);
assert.throws(
() => parseE29EvidenceCatalog(catalog({
items: [result({
linked_evidence: {
...result().linked_evidence,
published_on_control_plane: false,
},
})],
})),
E29EvidenceContractError,
);
});
test("fetches canonical catalog and exact review frame read-only", async () => {
const requests = [];
const fetchedCatalog = await fetchE29EvidenceCatalog({
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
return new Response(JSON.stringify(catalog()), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
});
const fetchedFrame = await fetchE29EvidenceFrame(resultId, 343, {
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
return new Response(JSON.stringify(frame()), {
status: 200,
headers: { "Content-Type": "application/json" },
});
},
});
assert.equal(fetchedCatalog.items[0].identity.frameCount, 4489);
assert.equal(fetchedFrame.sourceFrameIndex, 343);
assert.deepEqual(requests, [
{
input: "/api/v1/laboratory/e29/results?limit=1",
method: "GET",
},
{
input: `/api/v1/laboratory/e29/results/${resultId}/frames/343`,
method: "GET",
},
]);
});
+29
View File
@@ -34,6 +34,7 @@ from k1link.sessions import (
)
from k1link.web.device_plugin_composition import load_installed_device_plugins
from k1link.web.environment_api import build_environment_router
from k1link.web.laboratory_api import build_laboratory_router
from k1link.web.lidar_api import build_lidar_router
from k1link.web.plugin_catalog import DevicePluginCatalog, PluginCatalogError
from k1link.web.plugin_runtime import (
@@ -464,6 +465,34 @@ app.include_router(
),
)
)
app.include_router(
build_laboratory_router(
e29_root_provider=lambda: (
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e29" / "results"
),
local_surface_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "k1-local-surface-v1"
/ "models"
),
source_pack_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e10"
/ "lidar-packs"
),
source_result_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e10"
/ "worker-results"
),
)
)
frontend_dist = REPOSITORY_ROOT / "apps" / "control-station" / "dist"
+497
View File
@@ -0,0 +1,497 @@
from __future__ import annotations
import copy
import hashlib
import json
import re
from collections.abc import Callable, Mapping
from functools import lru_cache
from pathlib import Path
from typing import Any, Final
from fastapi import APIRouter, HTTPException, Query
from k1link.compute.semantic_geometry_fusion import (
CAMERA_GEOMETRY_FRAME_SCHEMA,
CAMERA_GEOMETRY_FRAMES_NAME,
CAMERA_GEOMETRY_FUSION_SCHEMA,
CAMERA_GEOMETRY_MANIFEST_NAME,
CAMERA_GEOMETRY_REPORT_NAME,
CAMERA_GEOMETRY_REPORT_SCHEMA,
)
LABORATORY_E29_CATALOG_SCHEMA: Final = (
"missioncore.laboratory-e29-catalog/v1"
)
LABORATORY_E29_DETAIL_SCHEMA: Final = "missioncore.laboratory-e29-detail/v1"
LABORATORY_E29_FRAME_SCHEMA: Final = "missioncore.laboratory-e29-frame/v1"
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_E29_RESULT_ID = re.compile(r"^e29-camera-geometry-[a-f0-9]{64}$")
_LOCAL_SURFACE_ID = re.compile(r"^k1-local-surface-[a-f0-9]{64}$")
_SOURCE_PACK_ID = re.compile(r"^e10-lidar-pack-[a-f0-9]{64}$")
_SOURCE_RESULT_ID = re.compile(r"^e10-integrated-perception-[a-f0-9]{64}$")
_SESSION_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$")
_MAX_MANIFEST_BYTES: Final = 256 * 1024
_MAX_REPORT_BYTES: Final = 2 * 1024 * 1024
_MAX_FRAME_BYTES: Final = 2 * 1024 * 1024
_MAX_REVIEW_FRAMES: Final = 64
RootProvider = Callable[[], Path | None]
class LaboratoryEvidenceError(ValueError):
"""Raised when published laboratory evidence is incomplete or changed."""
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _read_json(path: Path, *, maximum_bytes: int) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise LaboratoryEvidenceError("laboratory JSON artifact is unavailable")
size = path.stat().st_size
if size <= 0 or size > maximum_bytes:
raise LaboratoryEvidenceError("laboratory JSON artifact is out of bounds")
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise LaboratoryEvidenceError("laboratory JSON artifact is invalid") from exc
if not isinstance(value, dict):
raise LaboratoryEvidenceError("laboratory JSON artifact must be an object")
return value
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while chunk := stream.read(1024 * 1024):
digest.update(chunk)
return digest.hexdigest()
def _safe_artifact_path(root: Path, value: object) -> Path:
if not isinstance(value, str) or not value or Path(value).name != value:
raise LaboratoryEvidenceError("laboratory artifact path is invalid")
path = root / value
if path.is_symlink() or not path.is_file():
raise LaboratoryEvidenceError("laboratory artifact is unavailable")
return path
def _artifact_map(
root: Path,
manifest: Mapping[str, Any],
) -> dict[str, tuple[Path, Mapping[str, Any]]]:
raw = manifest.get("artifacts")
if not isinstance(raw, list) or len(raw) != 2:
raise LaboratoryEvidenceError("laboratory artifact list is invalid")
artifacts: dict[str, tuple[Path, Mapping[str, Any]]] = {}
for item in raw:
if not isinstance(item, dict):
raise LaboratoryEvidenceError("laboratory artifact entry is invalid")
role = item.get("role")
byte_length = item.get("byte_length")
digest = item.get("sha256")
if (
not isinstance(role, str)
or role in artifacts
or not isinstance(byte_length, int)
or byte_length <= 0
or not isinstance(digest, str)
or _SHA256.fullmatch(digest) is None
):
raise LaboratoryEvidenceError("laboratory artifact metadata is invalid")
path = _safe_artifact_path(root, item.get("path"))
if path.stat().st_size != byte_length:
raise LaboratoryEvidenceError("laboratory artifact size changed")
artifacts[role] = (path, item)
if set(artifacts) != {"camera-geometry-frames", "camera-geometry-report"}:
raise LaboratoryEvidenceError("laboratory artifact roles are invalid")
return artifacts
def _result_signature(root: Path) -> tuple[int, ...]:
paths = (
root / CAMERA_GEOMETRY_MANIFEST_NAME,
root / CAMERA_GEOMETRY_REPORT_NAME,
root / CAMERA_GEOMETRY_FRAMES_NAME,
)
signature: list[int] = []
for path in paths:
stat = path.stat()
signature.extend((stat.st_size, stat.st_mtime_ns))
return tuple(signature)
def _review_frame(row: Mapping[str, Any]) -> dict[str, object] | None:
observations = row.get("semantic_observations")
geometry = row.get("geometry_only_occupied")
if not isinstance(observations, list) or not isinstance(geometry, list):
raise LaboratoryEvidenceError("E29 frame collections are invalid")
conflicts = sum(
1
for observation in observations
if isinstance(observation, dict)
and observation.get("geometry_status") == "conflict"
)
if conflicts == 0:
return None
frame_index = row.get("frame_index")
session_seconds = row.get("session_seconds")
if (
not isinstance(frame_index, int)
or frame_index < 0
or not isinstance(session_seconds, (int, float))
):
raise LaboratoryEvidenceError("E29 review frame identity is invalid")
return {
"frame_index": frame_index,
"source_frame_index": row.get("source_frame_index"),
"session_seconds": float(session_seconds),
"conflict_count": conflicts,
"semantic_observation_count": len(observations),
"geometry_only_cluster_count": len(geometry),
}
@lru_cache(maxsize=16)
def _read_result_cached(
root_text: str,
signature: tuple[int, ...],
) -> tuple[dict[str, Any], tuple[int, ...]]:
del signature
root = Path(root_text)
if root.is_symlink() or not root.is_dir() or _E29_RESULT_ID.fullmatch(root.name) is None:
raise LaboratoryEvidenceError("E29 result id is invalid")
manifest = _read_json(
root / CAMERA_GEOMETRY_MANIFEST_NAME,
maximum_bytes=_MAX_MANIFEST_BYTES,
)
artifacts = _artifact_map(root, manifest)
report_path, report_artifact = artifacts["camera-geometry-report"]
frames_path, frames_artifact = artifacts["camera-geometry-frames"]
report = _read_json(report_path, maximum_bytes=_MAX_REPORT_BYTES)
identity = manifest.get("identity")
identity_sha256 = manifest.get("identity_sha256")
if (
manifest.get("schema_version") != CAMERA_GEOMETRY_FUSION_SCHEMA
or manifest.get("result_id") != root.name
or not isinstance(identity, dict)
or identity.get("schema_version") != CAMERA_GEOMETRY_FUSION_SCHEMA
or not isinstance(identity_sha256, str)
or _SHA256.fullmatch(identity_sha256) is None
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
or root.name != f"e29-camera-geometry-{identity_sha256}"
or report.get("schema_version") != CAMERA_GEOMETRY_REPORT_SCHEMA
or report.get("result_id") != root.name
or report.get("identity") != identity
or report.get("status") != "diagnostic-replay-complete"
or report.get("ground_truth") is not False
):
raise LaboratoryEvidenceError("E29 result identity is invalid")
if _sha256(report_path) != report_artifact.get("sha256"):
raise LaboratoryEvidenceError("E29 report digest changed")
expected_frames = identity.get("frame_count")
if not isinstance(expected_frames, int) or expected_frames <= 0:
raise LaboratoryEvidenceError("E29 frame count is invalid")
offsets: list[int] = []
review_frames: list[dict[str, object]] = []
digest = hashlib.sha256()
with frames_path.open("rb") as stream:
frame_index = 0
while True:
offset = stream.tell()
raw = stream.readline(_MAX_FRAME_BYTES + 1)
if not raw:
break
if len(raw) > _MAX_FRAME_BYTES:
raise LaboratoryEvidenceError("E29 frame is out of bounds")
digest.update(raw)
try:
row = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise LaboratoryEvidenceError("E29 frame is invalid") from exc
if (
not isinstance(row, dict)
or row.get("schema_version") != CAMERA_GEOMETRY_FRAME_SCHEMA
or row.get("frame_index") != frame_index
):
raise LaboratoryEvidenceError("E29 frame sequence changed")
offsets.append(offset)
if len(review_frames) < _MAX_REVIEW_FRAMES:
review = _review_frame(row)
if review is not None:
review_frames.append(review)
frame_index += 1
if (
len(offsets) != expected_frames
or digest.hexdigest() != frames_artifact.get("sha256")
):
raise LaboratoryEvidenceError("E29 frame evidence changed")
item = {
"result_id": root.name,
"created_at_utc": manifest.get("created_at_utc"),
"status": report["status"],
"identity": identity,
"metrics": report.get("metrics"),
"decision": report.get("decision"),
"limitations": report.get("limitations"),
"authority": report.get("authority"),
"review_frames": review_frames,
"ground_truth": False,
"access": "read-only",
}
return item, tuple(offsets)
def _linked_evidence(
item: Mapping[str, Any],
*,
local_surface_root: Path,
source_pack_root: Path,
source_result_root: Path,
) -> dict[str, object]:
identity = item.get("identity")
if not isinstance(identity, dict):
raise LaboratoryEvidenceError("E29 identity is unavailable")
model_id = identity.get("local_surface_model_id")
pack_id = identity.get("source_pack_id")
source_result_id = identity.get("source_result_id")
if (
not isinstance(model_id, str)
or _LOCAL_SURFACE_ID.fullmatch(model_id) is None
or not isinstance(pack_id, str)
or _SOURCE_PACK_ID.fullmatch(pack_id) is None
or not isinstance(source_result_id, str)
or _SOURCE_RESULT_ID.fullmatch(source_result_id) is None
):
raise LaboratoryEvidenceError("E29 linked evidence ids are invalid")
model_root = local_surface_root / model_id
pack_root = source_pack_root / pack_id
result_root = source_result_root / source_result_id
required = (
model_root / "manifest.json",
model_root / "local-surface.json",
model_root / "local-surface.npz",
pack_root / "manifest.json",
pack_root / "lidar-pack.npz",
result_root / "result.json",
result_root / "fusion-frames.jsonl",
)
if any(path.is_symlink() or not path.is_file() for path in required):
raise LaboratoryEvidenceError("E29 linked evidence is incomplete")
local_surface_report = _read_json(
model_root / "local-surface.json",
maximum_bytes=_MAX_REPORT_BYTES,
)
session_id = local_surface_report.get("session_id")
if (
local_surface_report.get("model_id") != model_id
or not isinstance(session_id, str)
or _SESSION_ID.fullmatch(session_id) is None
):
raise LaboratoryEvidenceError("E29 source session is invalid")
return {
"source_session_id": session_id,
"local_surface_model_id": model_id,
"source_pack_id": pack_id,
"source_result_id": source_result_id,
"published_on_control_plane": True,
"worker_copy_required": False,
}
def _roots(
*,
e29_root_provider: RootProvider,
local_surface_root_provider: RootProvider,
source_pack_root_provider: RootProvider,
source_result_root_provider: RootProvider,
) -> tuple[Path, Path, Path, Path] | None:
values = (
e29_root_provider(),
local_surface_root_provider(),
source_pack_root_provider(),
source_result_root_provider(),
)
if any(value is None for value in values):
return None
roots = tuple(value.resolve() for value in values if value is not None)
if len(roots) != 4 or any(not root.is_dir() for root in roots):
return None
return roots[0], roots[1], roots[2], roots[3]
def build_laboratory_router(
*,
e29_root_provider: RootProvider = lambda: None,
local_surface_root_provider: RootProvider = lambda: None,
source_pack_root_provider: RootProvider = lambda: None,
source_result_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
@router.get("/e29/results")
def list_e29_results(
limit: int = Query(default=1, ge=1, le=10),
) -> dict[str, object]:
roots = _roots(
e29_root_provider=e29_root_provider,
local_surface_root_provider=local_surface_root_provider,
source_pack_root_provider=source_pack_root_provider,
source_result_root_provider=source_result_root_provider,
)
if roots is None:
return {
"schema_version": LABORATORY_E29_CATALOG_SCHEMA,
"configured": False,
"items": [],
"candidate_total": 0,
"invalid_total": 0,
"access": "read-only",
}
e29_root, local_surface_root, source_pack_root, source_result_root = roots
candidates = sorted(
(
candidate
for candidate in e29_root.iterdir()
if candidate.is_dir()
and _E29_RESULT_ID.fullmatch(candidate.name) is not None
),
key=lambda candidate: candidate.stat().st_mtime_ns,
reverse=True,
)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in candidates:
try:
item, _ = _read_result_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
document = copy.deepcopy(item)
document["linked_evidence"] = _linked_evidence(
document,
local_surface_root=local_surface_root,
source_pack_root=source_pack_root,
source_result_root=source_result_root,
)
if len(items) < limit:
items.append(document)
except (LaboratoryEvidenceError, OSError):
invalid_total += 1
return {
"schema_version": LABORATORY_E29_CATALOG_SCHEMA,
"configured": True,
"items": items,
"candidate_total": len(candidates),
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get("/e29/results/{result_id}")
def get_e29_result(result_id: str) -> dict[str, object]:
roots = _roots(
e29_root_provider=e29_root_provider,
local_surface_root_provider=local_surface_root_provider,
source_pack_root_provider=source_pack_root_provider,
source_result_root_provider=source_result_root_provider,
)
if roots is None or _E29_RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="LAB E29 result не найден")
e29_root, local_surface_root, source_pack_root, source_result_root = roots
candidate = e29_root / result_id
if not candidate.is_dir():
raise HTTPException(status_code=404, detail="LAB E29 result не найден")
try:
item, _ = _read_result_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
document = copy.deepcopy(item)
document["linked_evidence"] = _linked_evidence(
document,
local_surface_root=local_surface_root,
source_pack_root=source_pack_root,
source_result_root=source_result_root,
)
return {
"schema_version": LABORATORY_E29_DETAIL_SCHEMA,
"result": document,
}
except (LaboratoryEvidenceError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="LAB E29 evidence не прошло проверку целостности",
) from exc
@router.get("/e29/results/{result_id}/frames/{frame_index}")
def get_e29_frame(result_id: str, frame_index: int) -> dict[str, object]:
roots = _roots(
e29_root_provider=e29_root_provider,
local_surface_root_provider=local_surface_root_provider,
source_pack_root_provider=source_pack_root_provider,
source_result_root_provider=source_result_root_provider,
)
if (
roots is None
or _E29_RESULT_ID.fullmatch(result_id) is None
or frame_index < 0
):
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
e29_root, local_surface_root, source_pack_root, source_result_root = roots
candidate = e29_root / result_id
if not candidate.is_dir():
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
try:
item, offsets = _read_result_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
_linked_evidence(
item,
local_surface_root=local_surface_root,
source_pack_root=source_pack_root,
source_result_root=source_result_root,
)
if frame_index >= len(offsets):
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
frames_path = candidate / CAMERA_GEOMETRY_FRAMES_NAME
with frames_path.open("rb") as stream:
stream.seek(offsets[frame_index])
raw = stream.readline(_MAX_FRAME_BYTES + 1)
if not raw or len(raw) > _MAX_FRAME_BYTES:
raise LaboratoryEvidenceError("E29 frame is out of bounds")
row = json.loads(raw)
if (
not isinstance(row, dict)
or row.get("schema_version") != CAMERA_GEOMETRY_FRAME_SCHEMA
or row.get("frame_index") != frame_index
):
raise LaboratoryEvidenceError("E29 frame identity changed")
return {
"schema_version": LABORATORY_E29_FRAME_SCHEMA,
"result_id": result_id,
"frame": row,
"access": "read-only",
}
except HTTPException:
raise
except (LaboratoryEvidenceError, OSError, json.JSONDecodeError) as exc:
raise HTTPException(
status_code=409,
detail="LAB E29 frame не прошёл проверку целостности",
) from exc
return router
+5 -1
View File
@@ -528,6 +528,8 @@ def build_lidar_router(
reverse=True,
)
for candidate in candidates:
if len(items) >= limit:
break
try:
model = K1LocalSurfaceV1(candidate)
try:
@@ -539,9 +541,11 @@ def build_lidar_router(
return {
"schema_version": K1_LOCAL_SURFACE_CATALOG_SCHEMA,
"configured": True,
"items": items[:limit],
"items": items,
"valid_total": len(items),
"invalid_total": invalid_total,
"candidate_total": len(candidates),
"scan_complete": len(items) + invalid_total == len(candidates),
"access": "read-only",
}
+248
View File
@@ -0,0 +1,248 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from fastapi import APIRouter
from fastapi.routing import APIRoute
from k1link.compute.semantic_geometry_fusion import (
CAMERA_GEOMETRY_FRAME_SCHEMA,
CAMERA_GEOMETRY_FRAMES_NAME,
CAMERA_GEOMETRY_FUSION_SCHEMA,
CAMERA_GEOMETRY_MANIFEST_NAME,
CAMERA_GEOMETRY_REPORT_NAME,
CAMERA_GEOMETRY_REPORT_SCHEMA,
)
from k1link.web.laboratory_api import build_laboratory_router
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode()
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _endpoint(router: APIRouter, path: str) -> object:
for route in router.routes:
if (
isinstance(route, APIRoute)
and route.path == path
and route.methods is not None
and "GET" in route.methods
):
return route.endpoint
raise AssertionError(f"GET {path} route is missing")
def _write_json(path: Path, value: object) -> None:
path.write_bytes(_canonical_json(value))
def _laboratory_evidence(tmp_path: Path) -> tuple[Path, Path, Path, Path, str]:
hash_a = "a" * 64
hash_b = "b" * 64
hash_c = "c" * 64
model_id = f"k1-local-surface-{hash_a}"
source_pack_id = f"e10-lidar-pack-{hash_b}"
source_result_id = f"e10-integrated-perception-{hash_c}"
identity = {
"schema_version": CAMERA_GEOMETRY_FUSION_SCHEMA,
"local_surface_model_id": model_id,
"source_pack_id": source_pack_id,
"source_result_id": source_result_id,
"frame_count": 2,
}
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
result_id = f"e29-camera-geometry-{identity_sha256}"
e29_root = tmp_path / "e29"
result_root = e29_root / result_id
result_root.mkdir(parents=True)
frames = [
{
"schema_version": CAMERA_GEOMETRY_FRAME_SCHEMA,
"frame_index": 0,
"source_frame_index": 100,
"session_seconds": 10.0,
"semantic_observations": [
{
"track_id": 7,
"semantic_class": "car",
"geometry_status": "conflict",
"range_m": 4.5,
}
],
"geometry_only_occupied": [],
},
{
"schema_version": CAMERA_GEOMETRY_FRAME_SCHEMA,
"frame_index": 1,
"source_frame_index": 101,
"session_seconds": 10.1,
"semantic_observations": [],
"geometry_only_occupied": [{"cluster_id": 8, "range_m": 2.0}],
},
]
frames_path = result_root / CAMERA_GEOMETRY_FRAMES_NAME
frames_path.write_bytes(b"".join(_canonical_json(row) + b"\n" for row in frames))
report = {
"schema_version": CAMERA_GEOMETRY_REPORT_SCHEMA,
"result_id": result_id,
"identity": identity,
"status": "diagnostic-replay-complete",
"ground_truth": False,
"metrics": {
"frames": {"total": 2},
"semantic_observations": {
"total": 1,
"geometry_agree": 0,
"camera_only": 0,
"conflict": 1,
},
"geometry_only": {"cluster_count": 1},
"runtime": {"postprocess_p95_ms": 1.2, "build_elapsed_ms": 3.4},
},
"decision": {"status": "replay-experiment-only"},
"limitations": ["synthetic evidence"],
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
},
}
report_path = result_root / CAMERA_GEOMETRY_REPORT_NAME
_write_json(report_path, report)
manifest = {
"schema_version": CAMERA_GEOMETRY_FUSION_SCHEMA,
"result_id": result_id,
"created_at_utc": "2026-07-26T00:00:00Z",
"identity": identity,
"identity_sha256": identity_sha256,
"artifacts": [
{
"role": "camera-geometry-frames",
"path": frames_path.name,
"byte_length": frames_path.stat().st_size,
"sha256": _sha256(frames_path),
},
{
"role": "camera-geometry-report",
"path": report_path.name,
"byte_length": report_path.stat().st_size,
"sha256": _sha256(report_path),
},
],
}
_write_json(result_root / CAMERA_GEOMETRY_MANIFEST_NAME, manifest)
local_surface_root = tmp_path / "local-surface"
model_root = local_surface_root / model_id
model_root.mkdir(parents=True)
_write_json(model_root / "manifest.json", {"model_id": model_id})
_write_json(
model_root / "local-surface.json",
{
"model_id": model_id,
"session_id": "20260720T065719Z_viewer_live",
},
)
(model_root / "local-surface.npz").write_bytes(b"verified-model")
source_pack_root = tmp_path / "source-pack"
pack_root = source_pack_root / source_pack_id
pack_root.mkdir(parents=True)
_write_json(pack_root / "manifest.json", {"pack_id": source_pack_id})
(pack_root / "lidar-pack.npz").write_bytes(b"verified-source-pack")
source_result_root = tmp_path / "source-result"
source_root = source_result_root / source_result_id
source_root.mkdir(parents=True)
_write_json(source_root / "result.json", {"result_id": source_result_id})
(source_root / "fusion-frames.jsonl").write_bytes(b"{}\n")
return (
e29_root,
local_surface_root,
source_pack_root,
source_result_root,
result_id,
)
def test_laboratory_catalog_and_frame_require_verified_linked_evidence(
tmp_path: Path,
) -> None:
e29_root, local_root, pack_root, source_root, result_id = (
_laboratory_evidence(tmp_path)
)
router = build_laboratory_router(
e29_root_provider=lambda: e29_root,
local_surface_root_provider=lambda: local_root,
source_pack_root_provider=lambda: pack_root,
source_result_root_provider=lambda: source_root,
)
catalog_route = _endpoint(router, "/api/v1/laboratory/e29/results")
frame_route = _endpoint(
router,
"/api/v1/laboratory/e29/results/{result_id}/frames/{frame_index}",
)
catalog = catalog_route(limit=1) # type: ignore[operator]
frame = frame_route(result_id=result_id, frame_index=0) # type: ignore[operator]
assert catalog["configured"] is True
assert catalog["candidate_total"] == 1
assert catalog["invalid_total"] == 0
assert len(catalog["items"]) == 1
item = catalog["items"][0]
assert item["result_id"] == result_id
assert item["linked_evidence"]["source_session_id"] == (
"20260720T065719Z_viewer_live"
)
assert item["linked_evidence"]["published_on_control_plane"] is True
assert frame["frame"]["semantic_observations"][0]["geometry_status"] == (
"conflict"
)
assert str(tmp_path) not in repr(catalog)
assert str(tmp_path) not in repr(frame)
def test_laboratory_catalog_hides_tampered_or_incomplete_results(
tmp_path: Path,
) -> None:
e29_root, local_root, pack_root, source_root, result_id = (
_laboratory_evidence(tmp_path)
)
frames_path = e29_root / result_id / CAMERA_GEOMETRY_FRAMES_NAME
frames_path.write_bytes(frames_path.read_bytes() + b"{}\n")
router = build_laboratory_router(
e29_root_provider=lambda: e29_root,
local_surface_root_provider=lambda: local_root,
source_pack_root_provider=lambda: pack_root,
source_result_root_provider=lambda: source_root,
)
catalog_route = _endpoint(router, "/api/v1/laboratory/e29/results")
tampered = catalog_route(limit=1) # type: ignore[operator]
assert tampered["candidate_total"] == 1
assert tampered["invalid_total"] == 1
assert tampered["items"] == []
frames_path.write_bytes(frames_path.read_bytes()[:-3])
(
local_root
/ f"k1-local-surface-{'a' * 64}"
/ "local-surface.npz"
).unlink()
incomplete = catalog_route(limit=1) # type: ignore[operator]
assert incomplete["candidate_total"] == 1
assert incomplete["invalid_total"] == 1
assert incomplete["items"] == []