feat(perception): add E34 temporal occupied layer

This commit is contained in:
DCCONSTRUCTIONS
2026-07-27 16:55:40 +03:00
parent 95c6540691
commit 621084fcd6
22 changed files with 4732 additions and 104 deletions
@@ -93,6 +93,7 @@ export interface AdvancedLaboratoryResults {
e31: E31LaboratoryResult | null;
e32: E32LaboratoryResult | null;
e33: E33LaboratoryResult | null;
e34: E34TemporalLayerResult | null;
}
export class AdvancedLaboratoryContractError extends Error {
@@ -371,10 +372,15 @@ export async function fetchAdvancedLaboratoryResults({
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise<AdvancedLaboratoryResults> {
const [e31, e32, e33] = await Promise.all([
const [e31, e32, e33, e34] = await Promise.all([
fetchOne("/api/v1/laboratory/e31/results?limit=1", parseE31, fetcher, signal),
fetchOne("/api/v1/laboratory/e32/results?limit=1", parseE32, fetcher, signal),
fetchOne("/api/v1/laboratory/e33/results?limit=1", parseE33, fetcher, signal),
fetchE34TemporalLayerResult({ fetcher, signal }),
]);
return { e31, e32, e33 };
return { e31, e32, e33, e34 };
}
import {
fetchE34TemporalLayerResult,
type E34TemporalLayerResult,
} from "./e34TemporalLayer";
@@ -0,0 +1,480 @@
export type E34TemporalState = "current" | "held" | "expired";
export type E34OccupancyState = "occupied" | "unknown";
export type E34OwnerKind = "camera-track" | "geometry-cluster";
export type E34Point3 = readonly [number, number, number];
export interface E34TemporalHistoryPoint {
frameIndex: number;
sessionSeconds: number;
centroidMapXyzM: E34Point3;
}
export interface E34TemporalComponent {
temporalId: number;
state: E34TemporalState;
occupancyState: E34OccupancyState;
ownerKind: E34OwnerKind;
centroidMapXyzM: E34Point3;
lastObservedAgeSeconds: number;
associationReason: string;
history: readonly E34TemporalHistoryPoint[];
}
export interface E34TemporalReviewFrame {
frameIndex: number;
sessionSeconds: number;
sourceAvailable: boolean;
layerState: "current" | "held" | "unknown";
counts: {
current: number;
held: number;
expired: number;
};
cellCentersMapXyzM: readonly E34Point3[];
components: readonly E34TemporalComponent[];
}
export interface E34TemporalLayerResult {
resultId: string;
createdAtUtc: string | null;
sourceSessionId: string;
status: "accepted-bounded-occupied-unknown-temporal-layer";
e32ResultId: string;
e33ResultId: string;
profileId: string;
pipelineId: string;
coordinateFrame: "map";
configuration: {
voxelSizeM: number;
occupiedTtlSeconds: number;
maximumActiveComponents: number;
maximumCellsPerComponent: number;
geometryMaximumGapSeconds: number;
geometryMaximumCentroidDistanceM: number;
};
metrics: {
sourceFrames: number;
processedFrames: number;
framesWithCurrentLayer: number;
heldOnlyFrames: number;
unknownEmptyFrames: number;
createdComponents: number;
cameraCreatedComponents: number;
geometryCreatedComponents: number;
geometryReassociatedComponents: number;
exactCameraAssociations: number;
geometrySpatialReassociations: number;
heldPublications: number;
expiredComponents: number;
peakActiveComponents: number;
maximumObservedSpanSeconds: number;
continuityFraction: number;
geometryOnlyReassociationFraction: number;
e32CurrentPointRows: number;
e34ConsumedCurrentPointRows: number;
currentCellRows: number;
heldCellRows: number;
storedCellRows: number;
freeCellRows: number;
peakCellsPerComponent: number;
maximumHeldAgeSeconds: number;
maximumExpiryDelaySeconds: number;
maximumReplayMaterializationDelaySeconds: number;
mapFrameJumpCandidates: number;
frameProcessingP95Ms: number;
buildElapsedMs: number;
};
reviewFrames: readonly E34TemporalReviewFrame[];
access: "read-only";
}
type LaboratoryFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
class E34TemporalLayerContractError extends Error {
constructor(message: string) {
super(message);
this.name = "E34TemporalLayerContractError";
}
}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new E34TemporalLayerContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function list(value: unknown, label: string): readonly unknown[] {
if (!Array.isArray(value)) {
throw new E34TemporalLayerContractError(`${label}: ожидался массив.`);
}
return value;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new E34TemporalLayerContractError(`${label}: ожидалась строка.`);
}
return value;
}
function optionalString(value: unknown, label: string): string | null {
return value === null ? null : stringValue(value, label);
}
function finiteNumber(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new E34TemporalLayerContractError(`${label}: ожидалось число.`);
}
return value;
}
function numberValue(value: unknown, label: string): number {
const parsed = finiteNumber(value, label);
if (parsed < 0) {
throw new E34TemporalLayerContractError(`${label}: число отрицательно.`);
}
return parsed;
}
function integerValue(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isSafeInteger(parsed)) {
throw new E34TemporalLayerContractError(`${label}: ожидалось целое число.`);
}
return parsed;
}
function booleanValue(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new E34TemporalLayerContractError(`${label}: ожидался boolean.`);
}
return value;
}
function exactString<T extends string>(
value: unknown,
expected: T,
label: string,
): T {
if (value !== expected) {
throw new E34TemporalLayerContractError(`${label}: неверное значение.`);
}
return expected;
}
function oneOf<T extends string>(
value: unknown,
expected: readonly T[],
label: string,
): T {
if (typeof value !== "string" || !expected.includes(value as T)) {
throw new E34TemporalLayerContractError(`${label}: неверное значение.`);
}
return value as T;
}
function contentId(value: unknown, prefix: string, label: string): string {
const parsed = stringValue(value, label);
if (!new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(parsed)) {
throw new E34TemporalLayerContractError(`${label}: неверный content id.`);
}
return parsed;
}
function point(value: unknown, label: string): E34Point3 {
const coordinates = list(value, label);
if (coordinates.length !== 3) {
throw new E34TemporalLayerContractError(`${label}: ожидались XYZ.`);
}
return [
finiteNumber(coordinates[0], `${label}[0]`),
finiteNumber(coordinates[1], `${label}[1]`),
finiteNumber(coordinates[2], `${label}[2]`),
];
}
function parseHistory(value: unknown, label: string): E34TemporalHistoryPoint {
const item = record(value, label);
return {
frameIndex: integerValue(item.frame_index, `${label}.frame_index`),
sessionSeconds: numberValue(
item.session_seconds,
`${label}.session_seconds`,
),
centroidMapXyzM: point(
item.centroid_map_xyz_m,
`${label}.centroid_map_xyz_m`,
),
};
}
function parseComponent(value: unknown, label: string): E34TemporalComponent {
const item = record(value, label);
return {
temporalId: integerValue(item.temporal_id, `${label}.temporal_id`),
state: oneOf(
item.state,
["current", "held", "expired"] as const,
`${label}.state`,
),
occupancyState: oneOf(
item.occupancy_state,
["occupied", "unknown"] as const,
`${label}.occupancy_state`,
),
ownerKind: oneOf(
item.owner_kind,
["camera-track", "geometry-cluster"] as const,
`${label}.owner_kind`,
),
centroidMapXyzM: point(
item.centroid_map_xyz_m,
`${label}.centroid_map_xyz_m`,
),
lastObservedAgeSeconds: numberValue(
item.last_observed_age_seconds,
`${label}.last_observed_age_seconds`,
),
associationReason: stringValue(
item.association_reason,
`${label}.association_reason`,
),
history: list(item.history_tail, `${label}.history_tail`).map(
(entry, index) => parseHistory(entry, `${label}.history_tail[${index}]`),
),
};
}
function parseReviewFrame(
value: unknown,
label: string,
): E34TemporalReviewFrame {
const item = record(value, label);
const counts = record(item.counts, `${label}.counts`);
return {
frameIndex: integerValue(item.frame_index, `${label}.frame_index`),
sessionSeconds: numberValue(
item.session_seconds,
`${label}.session_seconds`,
),
sourceAvailable: booleanValue(
item.source_available,
`${label}.source_available`,
),
layerState: oneOf(
item.layer_state,
["current", "held", "unknown"] as const,
`${label}.layer_state`,
),
counts: {
current: integerValue(counts.current, `${label}.counts.current`),
held: integerValue(counts.held, `${label}.counts.held`),
expired: integerValue(counts.expired, `${label}.counts.expired`),
},
cellCentersMapXyzM: list(
item.cell_centers_map_xyz_m,
`${label}.cell_centers_map_xyz_m`,
).map((entry, index) => point(
entry,
`${label}.cell_centers_map_xyz_m[${index}]`,
)),
components: list(item.components, `${label}.components`).map(
(entry, index) => parseComponent(
entry,
`${label}.components[${index}]`,
),
),
};
}
function diagnosticAuthority(value: unknown, label: string): void {
const authority = record(value, label);
if (
authority.commands_enabled !== false
|| authority.navigation_or_safety_accepted !== false
) {
throw new E34TemporalLayerContractError(
`${label}: запрещённые полномочия.`,
);
}
}
function metricRecord(
value: Record<string, unknown>,
): E34TemporalLayerResult["metrics"] {
const metric = (key: string) => numberValue(
value[key],
`E34.metrics.${key}`,
);
const integer = (key: string) => integerValue(
value[key],
`E34.metrics.${key}`,
);
return {
sourceFrames: integer("source_frames"),
processedFrames: integer("processed_frames"),
framesWithCurrentLayer: integer("frames_with_current_layer"),
heldOnlyFrames: integer("held_only_frames"),
unknownEmptyFrames: integer("unknown_empty_frames"),
createdComponents: integer("created_components"),
cameraCreatedComponents: integer("camera_created_components"),
geometryCreatedComponents: integer("geometry_created_components"),
geometryReassociatedComponents: integer("geometry_reassociated_components"),
exactCameraAssociations: integer("exact_camera_associations"),
geometrySpatialReassociations: integer("geometry_spatial_reassociations"),
heldPublications: integer("held_publications"),
expiredComponents: integer("expired_components"),
peakActiveComponents: integer("peak_active_components"),
maximumObservedSpanSeconds: metric("maximum_observed_span_seconds"),
continuityFraction: metric("continuity_fraction"),
geometryOnlyReassociationFraction: metric(
"geometry_only_reassociation_fraction",
),
e32CurrentPointRows: integer("e32_current_point_rows"),
e34ConsumedCurrentPointRows: integer("e34_consumed_current_point_rows"),
currentCellRows: integer("current_cell_rows"),
heldCellRows: integer("held_cell_rows"),
storedCellRows: integer("stored_cell_rows"),
freeCellRows: integer("free_cell_rows"),
peakCellsPerComponent: integer("peak_cells_per_component"),
maximumHeldAgeSeconds: metric("maximum_held_age_seconds"),
maximumExpiryDelaySeconds: metric("maximum_expiry_delay_seconds"),
maximumReplayMaterializationDelaySeconds: metric(
"maximum_replay_materialization_delay_seconds",
),
mapFrameJumpCandidates: integer("map_frame_jump_candidates"),
frameProcessingP95Ms: metric("frame_processing_p95_ms"),
buildElapsedMs: metric("build_elapsed_ms"),
};
}
function parseResult(value: unknown): E34TemporalLayerResult {
const item = record(value, "E34");
const configuration = record(item.configuration, "E34.configuration");
const review = record(item.review, "E34.review");
const acceptance = record(item.acceptance, "E34.acceptance");
diagnosticAuthority(item.authority, "E34.authority");
if (acceptance.accepted !== true) {
throw new E34TemporalLayerContractError("E34.acceptance: результат отклонён.");
}
exactString(
review.schema_version,
"missioncore.e34-temporal-review-timeline/v1",
"E34.review.schema_version",
);
return {
resultId: contentId(
item.result_id,
"e34-temporal-occupied",
"E34.result_id",
),
createdAtUtc: optionalString(item.created_at_utc, "E34.created_at_utc"),
sourceSessionId: stringValue(
item.source_session_id,
"E34.source_session_id",
),
status: exactString(
item.status,
"accepted-bounded-occupied-unknown-temporal-layer",
"E34.status",
),
e32ResultId: contentId(
item.e32_result_id,
"e32-track-geometry",
"E34.e32_result_id",
),
e33ResultId: contentId(
item.e33_result_id,
"e33-worker-shadow",
"E34.e33_result_id",
),
profileId: stringValue(item.profile_id, "E34.profile_id"),
pipelineId: stringValue(item.pipeline_id, "E34.pipeline_id"),
coordinateFrame: exactString(
item.coordinate_frame,
"map",
"E34.coordinate_frame",
),
configuration: {
voxelSizeM: numberValue(
configuration.voxel_size_m,
"E34.configuration.voxel_size_m",
),
occupiedTtlSeconds: numberValue(
configuration.occupied_ttl_seconds,
"E34.configuration.occupied_ttl_seconds",
),
maximumActiveComponents: integerValue(
configuration.maximum_active_components,
"E34.configuration.maximum_active_components",
),
maximumCellsPerComponent: integerValue(
configuration.maximum_cells_per_component,
"E34.configuration.maximum_cells_per_component",
),
geometryMaximumGapSeconds: numberValue(
configuration.geometry_maximum_gap_seconds,
"E34.configuration.geometry_maximum_gap_seconds",
),
geometryMaximumCentroidDistanceM: numberValue(
configuration.geometry_maximum_centroid_distance_m,
"E34.configuration.geometry_maximum_centroid_distance_m",
),
},
metrics: metricRecord(record(item.metrics, "E34.metrics")),
reviewFrames: list(review.frames, "E34.review.frames").map(
(entry, index) => parseReviewFrame(
entry,
`E34.review.frames[${index}]`,
),
),
access: exactString(item.access, "read-only", "E34.access"),
};
}
export async function fetchE34TemporalLayerResult({
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise<E34TemporalLayerResult | null> {
const response = await fetcher(
"/api/v1/laboratory/e34/results?limit=1",
{
method: "GET",
headers: { Accept: "application/json" },
signal,
},
);
if (!response.ok) {
throw new E34TemporalLayerContractError(
`Каталог LAB E34 недоступен: HTTP ${response.status}.`,
);
}
const catalog = record(await response.json(), "Каталог LAB E34");
exactString(
catalog.schema_version,
"missioncore.laboratory-advanced-catalog/v1",
"Каталог LAB E34.schema_version",
);
booleanValue(catalog.configured, "Каталог LAB E34.configured");
integerValue(catalog.candidate_total, "Каталог LAB E34.candidate_total");
integerValue(catalog.invalid_total, "Каталог LAB E34.invalid_total");
exactString(
catalog.access,
"read-only",
"Каталог LAB E34.access",
);
const items = list(catalog.items, "Каталог LAB E34.items");
if (items.length > 1) {
throw new E34TemporalLayerContractError(
"Каталог LAB E34: нарушен limit=1.",
);
}
return items.length ? parseResult(items[0]) : null;
}
+1
View File
@@ -3,6 +3,7 @@
@import "./styles/workspaces.css";
@import "./styles/laboratory.css";
@import "./styles/laboratory-reporting.css";
@import "./styles/e34-temporal-layer.css";
@import "./styles/e30-human-review.css";
@import "./styles/spatial.css";
@import "./styles/device.css";
@@ -0,0 +1,169 @@
.e34-temporal-evidence,
.e34-temporal-scene {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.e34-temporal-scene {
position: relative;
overflow: hidden;
background: var(--nodedc-canvas);
}
.e34-temporal-scene__viewport {
position: absolute;
inset: 0;
}
.e34-temporal-scene__viewport canvas {
display: block;
width: 100%;
height: 100%;
cursor: grab;
touch-action: none;
}
.e34-temporal-scene__viewport canvas:active {
cursor: grabbing;
}
.e34-temporal-scene__toolbar {
position: absolute;
z-index: 3;
top: 0.6rem;
left: 0.6rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.e34-temporal-scene__toolbar .nodedc-button {
background: var(--nodedc-floating-surface);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e34-temporal-scene__gestures,
.e34-temporal-scene__legend,
.e34-temporal-evidence__telemetry {
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-floating-surface);
color: var(--nodedc-text-secondary);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e34-temporal-scene__gestures {
display: flex;
gap: 0.55rem;
padding: 0.43rem 0.55rem;
}
.e34-temporal-scene__gestures span,
.e34-temporal-scene__legend span,
.e34-temporal-evidence__telemetry dt,
.e34-temporal-evidence__telemetry dd {
font-size: 0.5rem;
}
.e34-temporal-scene__legend {
position: absolute;
right: 0.6rem;
bottom: 0.6rem;
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
padding: 0.42rem 0.55rem;
}
.e34-temporal-scene__legend span {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.e34-temporal-scene__legend span::before {
width: 0.38rem;
height: 0.38rem;
border-radius: 50%;
background: var(--nodedc-text-muted);
content: "";
}
.e34-temporal-scene__legend span[data-state="current"]::before {
background: rgb(var(--nodedc-accent-rgb));
}
.e34-temporal-scene__legend span[data-state="held"]::before {
background: rgb(var(--nodedc-warning-rgb));
}
.e34-temporal-scene__legend span[data-state="expired"]::before {
background: rgb(var(--nodedc-danger-rgb));
}
.e34-temporal-evidence__telemetry {
position: absolute;
z-index: 3;
left: 0.6rem;
bottom: 0.6rem;
display: grid;
width: min(13rem, calc(100% - 1.2rem));
gap: 0.35rem;
margin: 0;
padding: 0.55rem 0.65rem;
pointer-events: none;
}
.e34-temporal-evidence__telemetry > div {
display: grid;
gap: 0.12rem;
}
.e34-temporal-evidence__telemetry dt,
.e34-temporal-evidence__telemetry dd {
margin: 0;
}
.e34-temporal-evidence__telemetry dt {
color: var(--nodedc-text-muted);
}
.e34-temporal-evidence__telemetry dd {
overflow: hidden;
color: var(--nodedc-text-primary);
font-weight: 650;
text-overflow: ellipsis;
white-space: nowrap;
}
.e34-temporal-evidence .laboratory-evidence-viewer__controls
.nodedc-select-anchor {
width: clamp(12rem, 24vw, 20rem);
}
.e34-temporal-scene__error {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--nodedc-text-muted);
font-size: 0.62rem;
}
@media (max-width: 900px) {
.e34-temporal-scene__gestures {
display: none;
}
.e34-temporal-scene__legend {
left: 0.6rem;
right: auto;
bottom: 5.7rem;
}
.e34-temporal-evidence .laboratory-evidence-viewer__controls
.nodedc-select-anchor {
width: 10rem;
}
}
@@ -0,0 +1,146 @@
import type { ComponentType } from "react";
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
import type { AdvancedLaboratoryResults } from "../../core/laboratory/advancedResults";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
import type { WorkspaceRendererProps } from "../contracts";
import { E31Result } from "./E31Result";
import { E32Result } from "./E32Result";
import { E33Result } from "./E33Result";
import { E34Result } from "./E34Result";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
export type AdvancedLaboratoryWorkId =
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
| "e34-temporal-layer";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
export function isAdvancedLaboratoryWorkId(
value: string,
): value is AdvancedLaboratoryWorkId {
return (
value === "e31-source-binding"
|| value === "e32-track-geometry"
|| value === "e33-worker-shadow"
|| value === "e34-temporal-layer"
);
}
export function advancedLaboratoryWorkOptions(
results: AdvancedLaboratoryResults,
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>,
): readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] {
const options: LaboratoryOption<AdvancedLaboratoryWorkId>[] = [];
if (results.e31 && sourceSessions.has(results.e31.sourceSessionId)) {
options.push({
id: "e31-source-binding",
label: "LAB E31 · source binding",
});
}
if (results.e32 && sourceSessions.has(results.e32.sourceSessionId)) {
options.push({
id: "e32-track-geometry",
label: "LAB E32 · TrackGeometry v1",
});
}
if (results.e33 && sourceSessions.has(results.e33.sourceSessionId)) {
options.push({
id: "e33-worker-shadow",
label: "LAB E33 · worker shadow 1×",
});
}
if (results.e34) {
options.push({
id: "e34-temporal-layer",
label: "LAB E34 · temporal occupied/unknown",
});
}
return options;
}
export function advancedLaboratorySourceSession(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>,
): ObservationSessionSummary | null {
const sourceSessionId = workId === "e31-source-binding"
? results.e31?.sourceSessionId
: workId === "e32-track-geometry"
? results.e32?.sourceSessionId
: workId === "e33-worker-shadow"
? results.e33?.sourceSessionId
: null;
return sourceSessionId ? sourceSessions.get(sourceSessionId) ?? null : null;
}
export function AdvancedLaboratoryResult({
props,
rigLabel,
workId,
results,
sourceSessions,
replayingSessionId,
failedSessionId,
replayError,
}: {
props: LaboratoryWorkspaceProps;
rigLabel: string;
workId: AdvancedLaboratoryWorkId;
results: AdvancedLaboratoryResults;
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>;
replayingSessionId: string | null;
failedSessionId: string | null;
replayError: string | null;
}) {
if (workId === "e34-temporal-layer" && results.e34) {
return <E34Result rigLabel={rigLabel} result={results.e34} />;
}
const sourceSession = advancedLaboratorySourceSession(
workId,
results,
sourceSessions,
);
if (!sourceSession) return null;
const evidence = (
<RecordedReplayEvidence
props={props}
sourceSession={sourceSession}
loading={replayingSessionId === sourceSession.id}
error={failedSessionId === sourceSession.id ? replayError : null}
/>
);
if (workId === "e31-source-binding" && results.e31) {
return (
<E31Result
rigLabel={rigLabel}
result={results.e31}
evidence={evidence}
/>
);
}
if (workId === "e32-track-geometry" && results.e32) {
return (
<E32Result
rigLabel={rigLabel}
result={results.e32}
evidence={evidence}
/>
);
}
if (workId === "e33-worker-shadow" && results.e33) {
return (
<E33Result
rigLabel={rigLabel}
result={results.e33}
evidence={evidence}
/>
);
}
return null;
}
@@ -0,0 +1,236 @@
import { useMemo, useState } from "react";
import { Select } from "@nodedc/ui-react";
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import type {
E34TemporalLayerResult,
E34TemporalReviewFrame,
} from "../../core/laboratory/e34TemporalLayer";
import { formatNumber } from "../../presentation";
import {
E34TemporalLayerScene,
type E34TemporalViewMode,
} from "./E34TemporalLayerScene";
function percent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})}%`;
}
function defaultFrame(
frames: readonly E34TemporalReviewFrame[],
): E34TemporalReviewFrame | null {
return frames.find((frame) => (
frame.counts.current > 0 && frame.counts.held > 0
)) ?? frames.find((frame) => frame.counts.expired > 0) ?? frames[0] ?? null;
}
function frameLabel(frame: E34TemporalReviewFrame): string {
const state = frame.counts.expired
? `истекло ${frame.counts.expired}`
: frame.counts.held
? `удерживается ${frame.counts.held}`
: `наблюдается ${frame.counts.current}`;
return `Кадр ${formatNumber(frame.frameIndex, 0)} · ${state}`;
}
function E34Evidence({
result,
}: {
result: E34TemporalLayerResult;
}) {
const initial = useMemo(
() => defaultFrame(result.reviewFrames),
[result.reviewFrames],
);
const [frameIndex, setFrameIndex] = useState(initial?.frameIndex ?? 0);
const [mode, setMode] = useState<E34TemporalViewMode>("3d");
const [expanded, setExpanded] = useState(false);
const frame = result.reviewFrames.find(
(item) => item.frameIndex === frameIndex,
) ?? initial;
if (!frame) {
return (
<div className="laboratory-result-pending" role="status">
Контрольные состояния временного слоя не опубликованы.
</div>
);
}
return (
<div className="e34-temporal-evidence">
<LaboratoryEvidenceViewer
label="Временной occupied/unknown слой E34"
mode={mode}
modes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "План" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<Select
label="Контрольное состояние E34"
value={String(frame.frameIndex)}
options={result.reviewFrames.map((item) => ({
value: String(item.frameIndex),
label: frameLabel(item),
}))}
variant="split"
menuWidth="anchor"
onChange={(value) => setFrameIndex(Number(value))}
/>
)}
overlay={(
<dl className="e34-temporal-evidence__telemetry">
<div>
<dt>Кадр / время</dt>
<dd>
{formatNumber(frame.frameIndex, 0)}
{" · "}
{frame.sessionSeconds.toLocaleString("ru-RU", {
maximumFractionDigits: 3,
})}
{" с"}
</dd>
</div>
<div>
<dt>Вход</dt>
<dd>{frame.sourceAvailable ? "Есть текущие точки" : "Текущих точек нет"}</dd>
</div>
<div>
<dt>Состояние слоя</dt>
<dd>
{frame.counts.current} current · {frame.counts.held} held · {frame.counts.expired} expired
</dd>
</div>
</dl>
)}
>
<E34TemporalLayerScene frame={frame} mode={mode} />
</LaboratoryEvidenceViewer>
</div>
);
}
export function E34Result({
rigLabel,
result,
}: {
rigLabel: string;
result: E34TemporalLayerResult;
}) {
const metrics = result.metrics;
const config = result.configuration;
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E34 · короткоживущий occupied/unknown слой"
description="Проверяли, может ли TrackGeometry поддерживать ограниченную во времени карту занятости: подтверждённые точки публикуются как occupied, исчезнувшее наблюдение кратко удерживается как unknown и затем удаляется строго по TTL."
status="Временной слой принят"
statusTone="success"
facts={[
{ label: "Источник", value: `${rigLabel} · TrackGeometry E32` },
{
label: "Объём проверки",
value: `${formatNumber(metrics.processedFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)} кадров`,
},
{
label: "Параметры слоя",
value: `${config.voxelSizeM.toLocaleString("ru-RU")} м · TTL ${config.occupiedTtlSeconds.toLocaleString("ru-RU")} с`,
},
{ label: "Полномочия", value: "Диагностика · команды и safety выключены" },
]}
brief={{
question: "Можно ли сохранить краткую пространственную непрерывность объектов между наблюдениями, не объявляя ненаблюдаемое пространство свободным и не загрязняя постоянную карту?",
approach: "Полная запись E32 воспроизведена через hit-only voxel layer в map-frame. Camera-track связывались только по точной идентичности, geometry-only компоненты — по ограниченному пространственно-временному сопоставлению. Первый immutable replay был отклонён; после исправления логических часов TTL и детектора глобального сдвига тот же замороженный профиль выполнен повторно.",
principalResult: `Да, в диагностическом контуре. Обработаны все ${formatNumber(metrics.processedFrames, 0)} кадров и все ${formatNumber(metrics.e34ConsumedCurrentPointRows, 0)} текущих точек; непрерывность наблюдений составила ${percent(metrics.continuityFraction)}, задержка логического истечения — ${metrics.maximumExpiryDelaySeconds.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с.`,
limitation: "Слой не вычисляет свободное пространство, не назначает dynamic/static, не меняет persistent reconstruction и не имеет навигационных или safety-полномочий. Непрерывность — диагностическая метрика, не ground truth.",
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "TrackGeometry → short-TTL hit-only occupied/unknown layer",
components: [
{
kind: "source",
name: "Принятый TrackGeometry из LAB E32",
version: `${formatNumber(metrics.e32CurrentPointRows, 0)} квалифицированных точек`,
role: "неизменяемый map-frame вход с явной принадлежностью точек",
identitySha256: null,
},
{
kind: "algorithm",
name: "Bounded temporal component association",
version: `voxel ${config.voxelSizeM} м · TTL ${config.occupiedTtlSeconds} с`,
role: "точное camera-связывание, geometry-only reassociation, hold и независимое expiry",
identitySha256: null,
},
{
kind: "runtime",
name: "Fail-closed diagnostic replay",
version: `${metrics.frameProcessingP95Ms.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} мс/frame p95`,
role: "полный учёт кадров, bounds, контроль map-frame и неизменность upstream",
identitySha256: null,
},
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ВРЕМЕННОЕ ДОКАЗАТЕЛЬСТВО"
title="Контрольные состояния: наблюдение, удержание unknown и точное истечение"
kind="diagnostic-model"
resizable
>
<E34Evidence result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Краткая непрерывность доказана; свободное пространство намеренно не выводится"
status="12 / 12 gate"
statusTone="success"
metrics={[
{
label: "Полный replay",
value: `${formatNumber(metrics.processedFrames, 0)} / ${formatNumber(metrics.sourceFrames, 0)}`,
hint: "все входные точки учтены",
},
{
label: "Связность компонентов",
value: percent(metrics.continuityFraction),
hint: `${formatNumber(metrics.exactCameraAssociations + metrics.geometrySpatialReassociations, 0)} повторных связей`,
},
{
label: "TTL / истечение",
value: `${metrics.maximumHeldAgeSeconds.toLocaleString("ru-RU", { maximumFractionDigits: 2 })} с`,
hint: `${metrics.maximumExpiryDelaySeconds.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с задержка`,
},
{
label: "Ограниченность",
value: `${formatNumber(metrics.peakActiveComponents, 0)} / ${formatNumber(config.maximumActiveComponents, 0)}`,
hint: `${formatNumber(metrics.peakCellsPerComponent, 0)} / ${formatNumber(config.maximumCellsPerComponent, 0)} ячеек`,
},
]}
conclusion={{
proved: "На данной полной записи hit-backed компоненты можно детерминированно удерживать до 0,75 с и удалять по независимому deadline, сохраняя bounds, полный учёт и неизменность E32/E33.",
notProved: "Работа не доказывает свободное пространство, динамический класс, качество планирования, переносимость порогов на другой сенсорный риг или корректность при реальном глобальном скачке map-frame.",
decision: "E34 принимается как read-only диагностический слой. Следующий критический gate — E35: детерминированно проверить деградацию и восстановление при потере источника, нарушении сроков и map-frame fault.",
}}
/>
)}
/>
);
}
@@ -0,0 +1,363 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Button, Icon } from "@nodedc/ui-react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import type {
E34Point3,
E34TemporalComponent,
E34TemporalReviewFrame,
} from "../../core/laboratory/e34TemporalLayer";
export type E34TemporalViewMode = "3d" | "plan";
function tokenColor(
host: HTMLElement,
token: string,
fallback: readonly [number, number, number],
): THREE.Color {
const value = getComputedStyle(host).getPropertyValue(token).trim();
if (value.startsWith("#")) {
return new THREE.Color(value);
}
const channels = value.match(/[\d.]+/g)?.slice(0, 3).map(Number);
const [red, green, blue] = channels?.length === 3
? channels
: fallback;
return new THREE.Color(red / 255, green / 255, blue / 255);
}
function disposeRenderable(object: THREE.Object3D): void {
const renderable = object as THREE.Object3D & {
geometry?: THREE.BufferGeometry;
material?: THREE.Material | THREE.Material[];
};
renderable.geometry?.dispose();
const materials = Array.isArray(renderable.material)
? renderable.material
: renderable.material
? [renderable.material]
: [];
materials.forEach((material) => material.dispose());
}
function scenePoint(
point: E34Point3,
origin: E34Point3,
): readonly [number, number, number] {
return [
point[0] - origin[0],
point[2] - origin[2],
-(point[1] - origin[1]),
];
}
function positions(
points: readonly E34Point3[],
origin: E34Point3,
): Float32Array {
const result = new Float32Array(points.length * 3);
points.forEach((point, index) => {
const [x, y, z] = scenePoint(point, origin);
const offset = index * 3;
result[offset] = x;
result[offset + 1] = y;
result[offset + 2] = z;
});
return result;
}
function median(values: readonly number[]): number {
if (!values.length) return 0;
const sorted = [...values].sort((left, right) => left - right);
return sorted[Math.floor(sorted.length / 2)] ?? 0;
}
function frameOrigin(frame: E34TemporalReviewFrame): E34Point3 {
const anchors = frame.components.length
? frame.components.map((component) => component.centroidMapXyzM)
: frame.cellCentersMapXyzM;
return [
median(anchors.map((point) => point[0])),
median(anchors.map((point) => point[1])),
median(anchors.map((point) => point[2])),
];
}
function componentColor(
host: HTMLElement,
component: E34TemporalComponent,
): THREE.Color {
if (component.state === "expired") {
return tokenColor(host, "--nodedc-danger-rgb", [255, 104, 112]);
}
if (component.state === "held") {
return tokenColor(host, "--nodedc-warning-rgb", [255, 209, 102]);
}
return tokenColor(host, "--nodedc-accent-rgb", [247, 248, 244]);
}
function fitRadius(frame: E34TemporalReviewFrame, origin: E34Point3): number {
const points = [
...frame.cellCentersMapXyzM,
...frame.components.map((component) => component.centroidMapXyzM),
];
if (!points.length) return 4;
const distances = points
.map((point) => {
const [x, y, z] = scenePoint(point, origin);
return Math.hypot(x, y, z);
})
.sort((left, right) => left - right);
const p95 = distances[Math.floor((distances.length - 1) * 0.95)] ?? 4;
return THREE.MathUtils.clamp(p95, 3, 32);
}
export function E34TemporalLayerScene({
frame,
mode,
}: {
frame: E34TemporalReviewFrame;
mode: E34TemporalViewMode;
}) {
const hostRef = useRef<HTMLDivElement | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
const controlsRef = useRef<OrbitControls | null>(null);
const contentRef = useRef<THREE.Group | null>(null);
const viewRadiusRef = useRef(5);
const [renderError, setRenderError] = useState<string | null>(null);
const origin = useMemo(() => frameOrigin(frame), [frame]);
useEffect(() => {
const host = hostRef.current;
if (!host) return;
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
powerPreference: "high-performance",
});
} catch {
setRenderError("Браузер не смог создать 3D-сцену временного слоя.");
return;
}
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(
tokenColor(host, "--nodedc-canvas", [5, 5, 6]),
1,
);
renderer.domElement.setAttribute(
"aria-label",
"Интерактивная 3D-сцена временного occupied/unknown слоя E34",
);
renderer.domElement.setAttribute("role", "img");
host.prepend(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(48, 1, 0.01, 500);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.enablePan = true;
controls.enableZoom = true;
controls.screenSpacePanning = true;
controls.minDistance = 0.5;
controls.maxDistance = 160;
controls.target.set(0, 0, 0);
const content = new THREE.Group();
scene.add(content);
sceneRef.current = scene;
cameraRef.current = camera;
controlsRef.current = controls;
contentRef.current = content;
const resize = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
};
const observer = new ResizeObserver(resize);
observer.observe(host);
resize();
let animationFrame = 0;
const render = () => {
animationFrame = window.requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
};
render();
return () => {
window.cancelAnimationFrame(animationFrame);
observer.disconnect();
controls.dispose();
scene.traverse(disposeRenderable);
renderer.dispose();
renderer.domElement.remove();
sceneRef.current = null;
cameraRef.current = null;
controlsRef.current = null;
contentRef.current = null;
};
}, []);
useEffect(() => {
const host = hostRef.current;
const content = contentRef.current;
if (!host || !content) return;
while (content.children.length) {
const child = content.children[0];
if (!child) continue;
content.remove(child);
child.traverse(disposeRenderable);
}
const contextGeometry = new THREE.BufferGeometry();
contextGeometry.setAttribute(
"position",
new THREE.BufferAttribute(
positions(frame.cellCentersMapXyzM, origin),
3,
),
);
const context = new THREE.Points(
contextGeometry,
new THREE.PointsMaterial({
color: tokenColor(host, "--nodedc-text-muted", [147, 151, 159]),
size: 2.4,
sizeAttenuation: false,
transparent: true,
opacity: 0.32,
depthWrite: false,
}),
);
content.add(context);
for (const component of frame.components) {
const color = componentColor(host, component);
const markerGeometry = new THREE.BufferGeometry();
markerGeometry.setAttribute(
"position",
new THREE.BufferAttribute(
positions([component.centroidMapXyzM], origin),
3,
),
);
const marker = new THREE.Points(
markerGeometry,
new THREE.PointsMaterial({
color,
size: component.state === "expired" ? 8 : 6,
sizeAttenuation: false,
transparent: true,
opacity: component.state === "expired" ? 0.72 : 1,
depthWrite: false,
}),
);
content.add(marker);
if (component.history.length > 1) {
const trailGeometry = new THREE.BufferGeometry();
trailGeometry.setAttribute(
"position",
new THREE.BufferAttribute(
positions(
component.history.map((item) => item.centroidMapXyzM),
origin,
),
3,
),
);
content.add(new THREE.Line(
trailGeometry,
new THREE.LineBasicMaterial({
color,
transparent: true,
opacity: component.state === "expired" ? 0.28 : 0.54,
}),
));
}
}
const radius = fitRadius(frame, origin);
viewRadiusRef.current = radius;
const grid = new THREE.GridHelper(
radius * 2.4,
20,
tokenColor(host, "--nodedc-text-muted", [96, 99, 106]),
tokenColor(host, "--nodedc-glass-outline", [48, 50, 56]),
);
const materials = Array.isArray(grid.material)
? grid.material
: [grid.material];
materials.forEach((material) => {
material.transparent = true;
material.opacity = 0.15;
material.depthWrite = false;
});
content.add(grid);
}, [frame, origin]);
const resetView = () => {
const camera = cameraRef.current;
const controls = controlsRef.current;
if (!camera || !controls) return;
const radius = viewRadiusRef.current;
controls.target.set(0, 0, 0);
if (mode === "plan") {
camera.position.set(0, radius * 2.8, 0.001);
camera.up.set(0, 0, -1);
} else {
camera.position.set(radius * 1.35, radius * 0.9, radius * 1.35);
camera.up.set(0, 1, 0);
}
camera.near = Math.max(radius / 2_000, 0.005);
camera.far = Math.max(radius * 20, 120);
camera.updateProjectionMatrix();
controls.maxDistance = Math.max(radius * 8, 40);
controls.update();
};
useEffect(resetView, [frame, mode]);
return (
<div className="e34-temporal-scene">
<div ref={hostRef} className="e34-temporal-scene__viewport">
{renderError ? (
<p className="e34-temporal-scene__error">{renderError}</p>
) : null}
</div>
<div className="e34-temporal-scene__toolbar">
<Button
variant="secondary"
size="compact"
icon={<Icon name="refresh" size={14} />}
onClick={resetView}
>
Сбросить ракурс
</Button>
<div className="e34-temporal-scene__gestures">
<span>ЛКМ · вращение</span>
<span>Колесо · масштаб</span>
<span>ПКМ · панорама</span>
</div>
</div>
<div className="e34-temporal-scene__legend">
<span data-state="cells">
Ячейки · {frame.cellCentersMapXyzM.length}
</span>
<span data-state="current">Наблюдается · {frame.counts.current}</span>
<span data-state="held">Удерживается · {frame.counts.held}</span>
<span data-state="expired">Истекло · {frame.counts.expired}</span>
</div>
</div>
);
}
@@ -29,9 +29,7 @@ import {
} from "../../core/laboratory/e30Review";
import {
fetchAdvancedLaboratoryResults,
type E31LaboratoryResult,
type E32LaboratoryResult,
type E33LaboratoryResult,
type AdvancedLaboratoryResults,
} from "../../core/laboratory/advancedResults";
import {
fetchLidarLocalSurfaces,
@@ -41,14 +39,17 @@ import { formatNumber } from "../../presentation";
import { E30ReviewWorkspace } from "../E30ReviewWorkspace";
import { LidarQualityWorkspace } from "../LidarQualityWorkspace";
import type { WorkspaceRendererProps } from "../contracts";
import { E31Result } from "./E31Result";
import { E32Result } from "./E32Result";
import { E33Result } from "./E33Result";
import {
AdvancedLaboratoryResult,
advancedLaboratorySourceSession,
advancedLaboratoryWorkOptions,
isAdvancedLaboratoryWorkId,
type AdvancedLaboratoryWorkId,
} from "./AdvancedLaboratoryResult";
import {
e28LaboratoryBrief, e29LaboratoryBrief,
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
} from "./laboratoryArchiveBriefs";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
@@ -58,11 +59,16 @@ type LaboratoryWorkId =
| "e28-local-surface"
| "e29-camera-geometry"
| "e30-evidence-review"
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
| AdvancedLaboratoryWorkId
| `session:${string}`;
const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
e31: null,
e32: null,
e33: null,
e34: null,
};
function digestFromContentId(value: string | null | undefined): string | null {
const digest = value?.split("-").at(-1) ?? "";
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
@@ -554,9 +560,9 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
const [e31Result, setE31Result] = useState<E31LaboratoryResult | null>(null);
const [e32Result, setE32Result] = useState<E32LaboratoryResult | null>(null);
const [e33Result, setE33Result] = useState<E33LaboratoryResult | null>(null);
const [advancedResults, setAdvancedResults] = useState(
EMPTY_ADVANCED_RESULTS,
);
const [evidenceLoading, setEvidenceLoading] = useState(true);
const [evidenceError, setEvidenceError] = useState<string | null>(null);
const sessions = useObservationSessions({
@@ -598,14 +604,12 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
setE28Model(nextE28);
setE29Result(nextE29);
setE30Result(nextE30);
setE31Result(nextAdvanced?.e31 ?? null);
setE32Result(nextAdvanced?.e32 ?? null);
setE33Result(nextAdvanced?.e33 ?? null);
setAdvancedResults(nextAdvanced ?? EMPTY_ADVANCED_RESULTS);
const failures = [
e28.status === "rejected" ? "E28" : null,
e29.status === "rejected" ? "E29" : null,
e30.status === "rejected" ? "E30" : null,
advanced.status === "rejected" ? "E31E33" : null,
advanced.status === "rejected" ? "E31E34" : null,
].filter(Boolean);
setEvidenceError(
failures.length
@@ -651,32 +655,16 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
label: "LAB E30 · evidence review A2",
});
}
if (e31Result && sourceSessions.has(e31Result.sourceSessionId)) {
items.push({
id: "e31-source-binding",
label: "LAB E31 · source binding",
});
}
if (e32Result && sourceSessions.has(e32Result.sourceSessionId)) {
items.push({
id: "e32-track-geometry",
label: "LAB E32 · TrackGeometry v1",
});
}
if (e33Result && sourceSessions.has(e33Result.sourceSessionId)) {
items.push({
id: "e33-worker-shadow",
label: "LAB E33 · worker shadow 1×",
});
}
items.push(...advancedLaboratoryWorkOptions(
advancedResults,
sourceSessions,
));
return items;
}, [
advancedResults,
e28Model,
e29Result,
e30Result,
e31Result,
e32Result,
e33Result,
sourceSessions,
]);
const profiles = useMemo(() => {
@@ -714,15 +702,6 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const e30SourceSession = e30Result
? sourceSessions.get(e30Result.sourceSessionId) ?? null
: null;
const e31SourceSession = e31Result
? sourceSessions.get(e31Result.sourceSessionId) ?? null
: null;
const e32SourceSession = e32Result
? sourceSessions.get(e32Result.sourceSessionId) ?? null
: null;
const e33SourceSession = e33Result
? sourceSessions.get(e33Result.sourceSessionId) ?? null
: null;
useEffect(() => {
if (
@@ -786,13 +765,13 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
void sessions.replay(e30SourceSession.id);
return;
}
const advancedSession = next === "e31-source-binding"
? e31SourceSession
: next === "e32-track-geometry"
? e32SourceSession
: next === "e33-worker-shadow"
? e33SourceSession
: null;
const advancedSession = isAdvancedLaboratoryWorkId(next)
? advancedLaboratorySourceSession(
next,
advancedResults,
sourceSessions,
)
: null;
if (advancedSession) {
void sessions.replay(advancedSession.id);
}
@@ -940,44 +919,16 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
result={e30Result}
sourceSession={e30SourceSession}
/>
) : workId === "e31-source-binding" && e31Result && e31SourceSession ? (
<E31Result
) : isAdvancedLaboratoryWorkId(workId) ? (
<AdvancedLaboratoryResult
props={props}
rigLabel={rigLabel}
result={e31Result}
evidence={(
<RecordedReplayEvidence
props={props}
sourceSession={e31SourceSession}
loading={sessions.replayingSessionId === e31SourceSession.id}
error={sessions.failedSessionId === e31SourceSession.id ? sessions.error : null}
/>
)}
/>
) : workId === "e32-track-geometry" && e32Result && e32SourceSession ? (
<E32Result
rigLabel={rigLabel}
result={e32Result}
evidence={(
<RecordedReplayEvidence
props={props}
sourceSession={e32SourceSession}
loading={sessions.replayingSessionId === e32SourceSession.id}
error={sessions.failedSessionId === e32SourceSession.id ? sessions.error : null}
/>
)}
/>
) : workId === "e33-worker-shadow" && e33Result && e33SourceSession ? (
<E33Result
rigLabel={rigLabel}
result={e33Result}
evidence={(
<RecordedReplayEvidence
props={props}
sourceSession={e33SourceSession}
loading={sessions.replayingSessionId === e33SourceSession.id}
error={sessions.failedSessionId === e33SourceSession.id ? sessions.error : null}
/>
)}
workId={workId}
results={advancedResults}
sourceSessions={sourceSessions}
replayingSessionId={sessions.replayingSessionId}
failedSessionId={sessions.failedSessionId}
replayError={sessions.error}
/>
) : selectedSession ? (
<PublishedLaboratoryResult
@@ -123,6 +123,89 @@ function e33() {
};
}
function e34() {
return {
result_id: `e34-temporal-occupied-${"4".repeat(64)}`,
created_at_utc: "2026-07-27T13:28:30Z",
source_session_id: "20260720T065719Z_viewer_live",
status: "accepted-bounded-occupied-unknown-temporal-layer",
e32_result_id: e32().result_id,
e33_result_id: e33().result_id,
profile_id: "e34-short-ttl-hit-only-occupied-unknown/v1",
pipeline_id: "lidar-local-map/short-ttl-hit-only/v1",
coordinate_frame: "map",
configuration: {
voxel_size_m: 0.45,
occupied_ttl_seconds: 0.75,
maximum_active_components: 256,
maximum_cells_per_component: 4096,
geometry_maximum_gap_seconds: 0.35,
geometry_maximum_centroid_distance_m: 0.9,
},
metrics: {
source_frames: 4489,
processed_frames: 4489,
frames_with_current_layer: 3928,
held_only_frames: 540,
unknown_empty_frames: 21,
created_components: 4912,
camera_created_components: 1027,
geometry_created_components: 3885,
geometry_reassociated_components: 2366,
exact_camera_associations: 5168,
geometry_spatial_reassociations: 17433,
held_publications: 39664,
expired_components: 4895,
peak_active_components: 37,
maximum_observed_span_seconds: 13.394,
continuity_fraction: 0.821466,
geometry_only_reassociation_fraction: 0.81776,
e32_current_point_rows: 2119302,
e34_consumed_current_point_rows: 2119302,
current_cell_rows: 834478,
held_cell_rows: 531581,
stored_cell_rows: 1366059,
free_cell_rows: 0,
peak_cells_per_component: 324,
maximum_held_age_seconds: 0.75,
maximum_expiry_delay_seconds: 0,
maximum_replay_materialization_delay_seconds: 0.283,
map_frame_jump_candidates: 0,
frame_processing_p95_ms: 1.483,
build_elapsed_ms: 4317,
},
review: {
schema_version: "missioncore.e34-temporal-review-timeline/v1",
result_id: `e34-temporal-occupied-${"4".repeat(64)}`,
frames: [{
frame_index: 238,
session_seconds: 59.206,
source_available: false,
layer_state: "unknown",
counts: { current: 0, held: 0, expired: 1 },
cell_centers_map_xyz_m: [],
components: [{
temporal_id: 136,
state: "expired",
occupancy_state: "unknown",
owner_kind: "camera-track",
centroid_map_xyz_m: [20.25, -2.83, 0.21],
last_observed_age_seconds: 0.75,
association_reason: "independent-ttl-deadline",
history_tail: [{
frame_index: 230,
session_seconds: 58.456,
centroid_map_xyz_m: [20.25, -2.83, 0.21],
}],
}],
}],
},
acceptance: { accepted: true },
authority,
access: "read-only",
};
}
before(async () => {
server = await createServer({
appType: "custom",
@@ -139,9 +222,9 @@ after(async () => {
await server?.close();
});
test("decodes E31, E32 and E33 from separate read-only catalogs", async () => {
test("decodes E31E34 from separate read-only catalogs", async () => {
const requests = [];
const items = [e31(), e32(), e33()];
const items = [e31(), e32(), e33(), e34()];
const decoded = await fetchAdvancedLaboratoryResults({
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
@@ -156,10 +239,13 @@ test("decodes E31, E32 and E33 from separate read-only catalogs", async () => {
assert.equal(decoded.e33.metrics.deliveredFrames, 4489);
assert.equal(decoded.e33.metrics.sourceAvailableFrames, 3928);
assert.equal(decoded.e33.metrics.resultAgeMaxMs, 13.638);
assert.equal(decoded.e34.metrics.processedFrames, 4489);
assert.equal(decoded.e34.reviewFrames[0].components[0].state, "expired");
assert.deepEqual(requests, [
{ input: "/api/v1/laboratory/e31/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e32/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e33/results?limit=1", method: "GET" },
{ input: "/api/v1/laboratory/e34/results?limit=1", method: "GET" },
]);
});
@@ -172,7 +258,8 @@ test("rejects authority escalation in an accepted-looking result", async () => {
() => fetchAdvancedLaboratoryResults({
fetcher: async (input) => new Response(JSON.stringify(catalog(
String(input).includes("/e31/") ? forged
: String(input).includes("/e32/") ? e32() : e33(),
: String(input).includes("/e32/") ? e32()
: String(input).includes("/e33/") ? e33() : e34(),
)), { status: 200 }),
}),
AdvancedLaboratoryContractError,
@@ -22,6 +22,14 @@ const e33ResultUrl = new URL(
"../src/workspaces/laboratory/E33Result.tsx",
import.meta.url,
);
const e34ResultUrl = new URL(
"../src/workspaces/laboratory/E34Result.tsx",
import.meta.url,
);
const advancedLaboratoryResultUrl = new URL(
"../src/workspaces/laboratory/AdvancedLaboratoryResult.tsx",
import.meta.url,
);
const workspacesUrl = new URL(
"../src/workspaces/Workspaces.tsx",
import.meta.url,
@@ -136,6 +144,23 @@ test("E33 explains the experiment, its method and its retained limits", async ()
assert.doesNotMatch(e33Source, /name: result\.e32ResultId/);
});
test("E34 keeps temporal evidence inside the canonical LAB and viewer contracts", async () => {
const [e34Source, advancedSource] = await Promise.all([
readFile(e34ResultUrl, "utf8"),
readFile(advancedLaboratoryResultUrl, "utf8"),
]);
assert.match(e34Source, /<LaboratoryWorkTemplate/);
assert.match(e34Source, /<LaboratoryEvidenceViewer/);
assert.match(e34Source, /\{ value: "3d", label: "3D" \}/);
assert.match(e34Source, /\{ value: "plan", label: "План" \}/);
assert.match(e34Source, /Первый immutable replay был отклонён/);
assert.match(e34Source, /Слой не вычисляет свободное пространство/);
assert.match(e34Source, /Следующий критический gate — E35/);
assert.match(advancedSource, /id: "e34-temporal-layer"/);
assert.match(advancedSource, /<E34Result/);
});
test("the primary point-cloud viewer restores from fullscreen on Escape", async () => {
const workspacesSource = await readFile(workspacesUrl, "utf8");
+16 -3
View File
@@ -681,10 +681,23 @@ workloads and are evidence of node visibility, not E33 process consumption.
E33 qualifies downstream publication against the immutable E10→E32 chain; it
does not rerun or independently requalify upstream inference.
Execution was strictly sequential through E33: E30 determined what E31 was
E34 accepted immutable temporal-layer result
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`.
It consumes all `2,119,302` accepted E32 point rows, keeps current occupied
cells separate from held/expired unknown state and preserves the persistent
reconstruction. Peak state is 37 components and 324 cells per component;
component continuity is `82.15%`, geometry-only re-association is `81.78%`,
logical TTL emission delay is zero and coherent global map-frame jump count is
zero. The earlier rejected immutable run exposed and retained a `0.333 s`
source gap plus an incoherent nearest-neighbor jump detector; the fixed
implementation uses an independent TTL deadline and coherent translation
support without changing the frozen profile.
Execution was strictly sequential through E34: E30 determined what E31 was
allowed to change; E31 determined the E32 profile; E32 determined the E33
runtime input. Exact accounting is now closed, so E34 and E35 are the active
critical path.
runtime input; E32/E33 then bound the E34 layer. Exact accounting and the
bounded nominal temporal layer are now closed, so E35 is the active critical
path.
E36 is the first generalization gate. A separate product decision follows:
either keep the result as operator/shadow evidence, or start L5 occupied-space
integration. No LAB in this cycle can enable navigation, commands or safety
@@ -192,7 +192,17 @@ p95 was `2.668 ms`, process RSS peaked at `92.34 MiB`, both queues remained
bounded at depth two and the exact physical worker/container identity is
retained. This qualifies the final TrackGeometry publication stage against the
immutable upstream E10→E32 chain; it is not a new model-inference benchmark.
A8/E34+E35 is now the critical path.
A8/E34 is complete in immutable result
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`.
It consumes all `2,119,302` accepted E32 point rows across all 4,489 frames,
keeps hit-only occupied evidence separate from persistent reconstruction,
publishes held/expired state as unknown, peaks at 37 active components and 324
cells per component, and emits no free cells. Geometry-only re-association is
`81.78%`; overall component continuity is `82.15%`. Expiration uses an
independent `0.75 s` deadline and records the `0.283 s` worst source-gap
materialization delay separately. The first immutable run remains a rejected
artifact documenting the frame-clock and incoherent-jump implementation
failures. E35 is now the critical path.
- [x] Reproduce all 4,489 immutable E29 frames with the exact frozen profile
before applying E31/E30 changes.
@@ -212,7 +222,7 @@ A8/E34+E35 is now the critical path.
and publish per-frame health/timing plus resource telemetry.
- [x] Bind the accepted result to physical worker `DESKTOP-OPJ8J04` and the
pinned container image; independently verify all result artifacts.
- [ ] Build E34 as a separate short-TTL occupied/unknown temporal layer over
- [x] Build E34 as a separate short-TTL occupied/unknown temporal layer over
accepted E32/E33 evidence.
- [ ] Run E35 deterministic degradation/recovery variants without changing
the immutable source or accepted E32/E33 results.
@@ -0,0 +1,89 @@
# ADR 0027 — E34 short-TTL occupied/unknown temporal layer
Date: 2026-07-27
Status: accepted for source-scoped diagnostic/shadow use
## Context
E32 published exclusive, source-indexed `PointSlab` ownership over all 4,489
immutable RAVNOVES00 frames. E33 proved that the exact E32 publication stream
can be delivered at recorded `1.0×` pace through bounded worker channels with
closed accounting. Neither result is a local temporal model: geometry-only
component keys are frame-local, current support disappears immediately when a
source is unavailable, and there is no explicit expiration event.
The K1 stream is already registered mapped evidence. It does not retain the
beam origin and firing time required for honest ray-cleared free-space
mapping. The accepted persistent reconstruction must also remain a separate,
immutable derivative.
## Predeclared decision
1. E34 consumes only the accepted content-addressed E32 result under the
accepted E33 timing/accounting envelope. It never rewrites E32, E33, raw
evidence, the mapped stream or persistent reconstruction.
2. The output is a separate `lidar-local-map/v1` diagnostic derivative in the
E32 `map` frame. It publishes only hit-backed `occupied` and conservatively
aged `unknown` state. It never publishes `free`.
3. Current E32 point owners become `current/occupied` temporal components.
Camera track identity is retained only as source provenance. Geometry-only
components receive new temporal IDs and may be spatially re-associated
without inventing a semantic class.
4. A component without current accepted points becomes `held/unknown` for at
most `0.75 s`. Its last hit-backed cells remain explicitly stale evidence,
not current measurement and not free-space proof.
5. After the TTL, the component emits one `expired/unknown` tombstone and is
removed from the active occupied layer. Expired cells are never published
as occupied.
6. Geometry-only re-association is deterministic, one-to-one and bounded to a
`0.35 s` gap and `0.9 m` centroid distance. It additionally requires either
a `0.05` neighbor-expanded voxel overlap or a centroid distance no greater
than one `0.45 m` voxel.
7. The layer uses hit-only `0.45 m` voxels, at most `4,096` cells per component
and at most `256` active components. Crossing a bound fails closed; cells
are not silently truncated and components are not silently evicted.
8. A map-frame jump candidate is diagnostic only. It requires at least four
adjacent matched components within `0.25 s`, median displacement of at
least `1.5 m` and 25th-percentile displacement of at least `0.9 m`.
9. Acceptance requires complete 4,489-frame accounting, exact upstream
artifact identity, bounded state, no free-space publication, no component
held past TTL, expiry no later than one `0.25 s` observation interval after
the theoretical deadline and zero global map-frame jump candidates.
10. Continuity, re-association, geometry-only persistence and ghost lifetime
are measured and reported. They are not converted into detector accuracy
or ground truth and are not retuned after inspecting this run.
11. Dynamic class, traversability, planner input, commands, navigation and
safety authority remain unavailable.
## Consequences
- The E34 result can prove a bounded temporal contract without claiming
free-space quality or changing persistent mapping.
- Camera-only, conflict, unknown and source-unavailable inputs can preserve
uncertainty but cannot create new occupied cells without prior accepted
hit-backed provenance.
- A rejected run remains immutable evidence. Any later profile change requires
a new ADR decision, profile identity and LAB result rather than overwriting
E34.
## Execution outcome
The first immutable implementation result
`e34-temporal-occupied-1bab293c1867100e172923b189f8273c47eacca3dd179fd54573200906affd7c`
was rejected without changing the profile. It exposed two implementation-level
architecture errors:
- scalar nearest-neighbor displacement was not sufficient to call a jump
global; the corrected detector requires one coherent translation within the
already fixed voxel residual;
- frame-driven expiry could not meet the TTL across a recorded `0.333 s`
source gap; expiration now uses an independent logical layer deadline and
retains the later replay materialization time separately.
The accepted result is
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`.
It processes all 4,489 frames and all 2,119,302 E32 point rows, peaks at 37
active components and 324 cells per component, emits zero free cells, meets
the `0.75 s` TTL exactly and reports zero coherent global map-frame jump
candidates. A8 now continues with E35 deterministic degradation/recovery.
@@ -0,0 +1,182 @@
# LAB E34 — bounded short-TTL occupied/unknown temporal layer
Date: 2026-07-27
Status: accepted for source-scoped diagnostic/shadow use; dynamic class, free
space, planner, navigation, safety and command authority are unavailable
Immutable result:
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`
## Objective and architecture stage
E34 closes the occupied-layer half of A8. It asks whether accepted E32
hit-backed geometry can form a bounded local temporal model without changing
raw/map evidence or polluting persistent reconstruction.
The result is a separate `lidar-local-map/short-ttl-hit-only/v1` derivative. It makes
`current`, `held` and `expired` states explicit, spatially re-associates
frame-local geometry-only components, preserves camera identity only as source
provenance and never converts missing mapped points into free space.
## Immutable inputs and predeclared profile
- E32:
`e32-track-geometry-a14ca0e7fb3850ca0dfa3c41634e1b490a2d58ab74d101afc6d6921fbdb0e6fd`.
- Accepted E33 timing/accounting envelope:
`e33-worker-shadow-05cc0bb264410fd49536df90e94067ac39731aff0322a8873700d40008a8bb3a`.
- Source: RAVNOVES00 / `20260720T065719Z_viewer_live`.
- Frames: `4,489`.
- Timeline: `35.421857292484.044857292 s`.
- Coordinate frame: accepted E32 `map`.
- Profile:
`missioncore.e34-temporal-occupied-profile/v1`,
`e34-short-ttl-hit-only-occupied-unknown/v1`.
The profile and ADR 0027 were written before the first full replay:
- hit-only voxel size `0.45 m`;
- occupied TTL `0.75 s`;
- maximum `256` active components;
- maximum `4,096` cells per component;
- geometry-only re-association gap `0.35 s`;
- centroid gate `0.9 m`;
- neighbor-expanded voxel overlap minimum `0.05`;
- no free space, dynamic class or persistent-map mutation;
- navigation, safety and command authority false.
No threshold or bound was changed after inspecting either run.
## Method and algorithms
For every E32 frame the E34 layer:
1. reconstructs the exact validated `TrackGeometryFrame` and memory-mapped
`PointSlab`;
2. consumes only geometries whose metric basis is `current-points`;
3. voxelizes their exclusive map-frame point rows at `0.45 m`;
4. preserves an exact camera owner across frames without transferring semantic
ownership to E34;
5. re-associates geometry-only observations one-to-one using age, centroid and
neighbor-expanded voxel overlap;
6. publishes current hit-backed components as `current/occupied`;
7. publishes missing but not expired prior evidence as `held/unknown`;
8. emits an `expired/unknown` tombstone on the independent layer deadline and
removes the cells from active occupancy;
9. checks a global map jump only when at least four components support one
coherent translation; scalar nearest-neighbor distance alone is
insufficient;
10. records compact cell rows, complete per-frame state, track-history tails,
expiration events, review samples, metrics, acceptance and artifact
digests.
Bounds fail closed. Components and cells are never silently truncated or
evicted. Unknown camera-only/conflict/held observations without current E32
points cannot create occupied cells.
## Failed first run and architecture corrections
The first immutable result
`e34-temporal-occupied-1bab293c1867100e172923b189f8273c47eacca3dd179fd54573200906affd7c`
was rejected by two predeclared gates:
- seven scalar nearest-neighbor map-jump candidates;
- maximum frame-driven expiry delay `0.283 s`, above the `0.25 s` gate.
The result was not overwritten and the profile was not relaxed.
Inspection showed that the jump implementation did not yet implement the
word “global”: the flagged component displacement directions were
incoherent, and frames 1762/3190 contained an exact camera anchor moving only
`0.180.24 m`. The implementation was corrected to require a single coherent
translation with residual no larger than the already fixed `0.45 m` voxel.
The expiry failure exposed a real source gap: frame 1732 at
`208.513857292 s` is followed by frame 1733 at `208.846857292 s`. A
frame-driven state machine cannot emit a `0.75 s` TTL event during that
`0.333 s` gap. Expiration was therefore moved to an independent logical layer
deadline. The exact event time is retained separately from the later replay
materialization time. This is an architecture correction, not a larger TTL.
## Accepted result
Result:
`e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73`.
| Measure | Result | Gate |
| --- | ---: | ---: |
| processed frames | 4,489 / 4,489 | complete |
| E32 point rows consumed | 2,119,302 / 2,119,302 | exact |
| peak active components | 37 | ≤ 256 |
| peak cells/component | 324 | ≤ 4,096 |
| current / held cell rows | 834,478 / 531,581 | hit-backed / stale-unknown |
| free cell rows | 0 | 0 |
| component continuity | 82.15% | descriptive, not GT |
| geometry-only re-association | 81.78% | descriptive, not GT |
| components with geometry re-association | 2,366 | descriptive |
| maximum observed component span | 13.394 s | descriptive |
| maximum held age | 0.750 s | ≤ 0.750 s |
| logical expiry delay | 0.000 s | ≤ 0.250 s |
| maximum replay materialization delay | 0.283 s | retained source-gap evidence |
| coherent global map-jump candidates | 0 | 0 |
| frame processing p95 | 1.483 ms | diagnostic runtime |
The layer created 4,912 temporal components: 1,027 camera-provenance and 3,885
geometry-only. It recorded 5,168 exact camera associations, 17,433 spatial
geometry re-associations, 39,664 held publications and 4,895 expiration
events. The peak active state remained 37 components.
All predeclared acceptance requirements pass. Raw/map evidence, E32/E33 and
persistent reconstruction retain their exact artifact identities.
## Product materialization
The accepted result is exposed through the path-free read-only endpoint
`GET /api/v1/laboratory/e34/results?limit=1` and through a separate
`LAB E34 · temporal occupied/unknown` entry in the laboratory contour.
The UI uses the fixed `missioncore.laboratory-report/v1` anatomy:
- compact human-readable task, method, principal result and limitation;
- a domain 3D viewer with the admitted 3D/plan and fullscreen controls;
- 27 bounded review checkpoints for current, held and expired states;
- explicit current/held/expired counts, source availability and session time;
- a fixed result block with proved, not proved and decision statements.
The central laboratory workspace did not gain another experiment-specific
layout branch. E31E34 discovery and rendering are isolated in the advanced
laboratory feature registry; the E34 visualizer is a domain renderer inside
the existing laboratory evidence surface. No new generic Design Guideline
component or visual language was introduced.
## Interpretation and limitations
E34 proves:
- bounded current/held/expired occupied/unknown state for this source;
- lossless consumption of accepted E32 point rows;
- deterministic geometry-only temporal identity;
- independent TTL expiry;
- no hidden free-space claim or persistent-map write;
- enough compact evidence to inspect history and expiration.
E34 does not prove:
- detector accuracy or human ground truth;
- a dynamic/static class;
- ray-cleared free space, traversability, TSDF or ESDF;
- second-source generalization;
- correct behavior under injected source loss, delay, drops or offset;
- planner, navigation, safety or command authority.
The `0.283 s` replay materialization gap is deliberately retained. E35 must
exercise source-independent deadlines and recovery under deterministic loss,
staleness, delay, bounded drops and timing offsets.
## Decision and next stage
E34 is accepted as a source-scoped diagnostic/shadow temporal occupied/unknown
layer. A8 remains open only for E35.
The next critical-path work is E35 deterministic degradation and recovery
using immutable derived replay variants. Missing evidence must become
camera-only, geometry-only, stale or unknown; it must never become guessed
class, false free space or hidden success.
@@ -0,0 +1,45 @@
{
"schema_version": "missioncore.e34-temporal-occupied-profile/v1",
"profile_id": "e34-short-ttl-hit-only-occupied-unknown/v1",
"expected_e32_result_id": "e32-track-geometry-a14ca0e7fb3850ca0dfa3c41634e1b490a2d58ab74d101afc6d6921fbdb0e6fd",
"expected_e33_result_id": "e33-worker-shadow-05cc0bb264410fd49536df90e94067ac39731aff0322a8873700d40008a8bb3a",
"layer": {
"coordinate_frame": "map",
"voxel_size_m": 0.45,
"occupied_ttl_seconds": 0.75,
"maximum_active_components": 256,
"maximum_cells_per_component": 4096
},
"association": {
"geometry_maximum_gap_seconds": 0.35,
"geometry_maximum_centroid_distance_m": 0.9,
"geometry_minimum_voxel_overlap_fraction": 0.05,
"geometry_neighbor_radius_cells": 1
},
"map_frame_jump": {
"maximum_adjacent_gap_seconds": 0.25,
"minimum_matched_components": 4,
"minimum_median_displacement_m": 1.5,
"minimum_p25_displacement_m": 0.9
},
"acceptance": {
"maximum_expiry_delay_seconds": 0.25,
"maximum_map_frame_jump_candidates": 0,
"require_complete_frame_accounting": true,
"require_exact_upstream_artifact_identity": true,
"require_no_free_space_publication": true,
"require_bounded_state": true
},
"policy": {
"hit_only_occupied": true,
"absence_of_points_means_free": false,
"expired_components_remain_occupied": false,
"dynamic_class_available": false,
"free_space_available": false,
"persistent_reconstruction_mutation_allowed": false
},
"authority": {
"commands_enabled": false,
"navigation_or_safety_accepted": false
}
}
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Build the immutable E34 short-TTL occupied/unknown temporal layer."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from k1link.compute.e34_temporal_occupied_replay import (
build_e34_temporal_occupied_replay,
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--e32-result", type=Path, required=True)
parser.add_argument("--e33-result", type=Path, required=True)
parser.add_argument("--profile", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
args = parser.parse_args()
result = build_e34_temporal_occupied_replay(
e32_result_root=args.e32_result,
e33_result_root=args.e33_result,
profile_path=args.profile,
output_root=args.output_root,
)
print(
json.dumps(
{
"result_id": result.result_id,
"result_root": str(result.result_root),
"accepted": result.accepted,
"metrics": result.report["metrics"],
"rejection_reasons": result.report["acceptance"][
"rejection_reasons"
],
},
ensure_ascii=False,
sort_keys=True,
)
)
return 0 if result.accepted else 2
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+158
View File
@@ -24,6 +24,11 @@ from k1link.compute.e33_worker_shadow import (
E33WorkerShadowResult,
read_e33_worker_shadow_result,
)
from k1link.compute.e34_temporal_occupied_replay import (
E34TemporalOccupiedReplay,
E34TemporalOccupiedReplayError,
read_e34_temporal_occupied_replay,
)
LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
"missioncore.laboratory-advanced-catalog/v1"
@@ -32,6 +37,7 @@ LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
_E31_RESULT_ID = re.compile(r"^e31-source-qualification-[a-f0-9]{64}$")
_E32_RESULT_ID = re.compile(r"^e32-track-geometry-[a-f0-9]{64}$")
_E33_RESULT_ID = re.compile(r"^e33-worker-shadow-[a-f0-9]{64}$")
_E34_RESULT_ID = re.compile(r"^e34-temporal-occupied-[a-f0-9]{64}$")
RootProvider = Callable[[], Path | None]
@@ -73,6 +79,15 @@ def _read_e33_cached(
return read_e33_worker_shadow_result(Path(root_text))
@lru_cache(maxsize=16)
def _read_e34_cached(
root_text: str,
signature: tuple[int, ...],
) -> E34TemporalOccupiedReplay:
del signature
return read_e34_temporal_occupied_replay(Path(root_text))
def _configured_root(provider: RootProvider) -> Path | None:
value = provider()
if value is None:
@@ -294,6 +309,113 @@ def _project_e33(
}
def _project_e34(result: E34TemporalOccupiedReplay) -> dict[str, object]:
identity = _object(result.manifest.get("identity"), "E34 identity")
profile = _object(identity.get("profile"), "E34 profile")
layer = _object(profile.get("layer"), "E34 layer")
association = _object(profile.get("association"), "E34 association")
metrics = _object(result.report.get("metrics"), "E34 metrics")
frames = _object(metrics.get("frames"), "E34 frames")
components = _object(metrics.get("components"), "E34 components")
occupancy = _object(metrics.get("occupancy"), "E34 occupancy")
aging = _object(metrics.get("aging"), "E34 aging")
map_frame = _object(metrics.get("map_frame"), "E34 map frame")
runtime = _object(metrics.get("runtime"), "E34 runtime")
frame_processing = _object(
runtime.get("frame_processing_ms"),
"E34 frame processing",
)
acceptance = _object(result.report.get("acceptance"), "E34 acceptance")
if acceptance.get("accepted") is not True:
raise ValueError("E34 result is not accepted")
return {
"result_id": result.result_id,
"created_at_utc": result.manifest.get("created_at_utc"),
"source_session_id": identity.get("source_session_id"),
"status": result.manifest.get("acceptance_state"),
"e32_result_id": identity.get("e32_result_id"),
"e33_result_id": identity.get("e33_result_id"),
"profile_id": profile.get("profile_id"),
"pipeline_id": identity.get("pipeline"),
"coordinate_frame": identity.get("coordinate_frame"),
"configuration": {
"voxel_size_m": layer.get("voxel_size_m"),
"occupied_ttl_seconds": layer.get("occupied_ttl_seconds"),
"maximum_active_components": layer.get(
"maximum_active_components"
),
"maximum_cells_per_component": layer.get(
"maximum_cells_per_component"
),
"geometry_maximum_gap_seconds": association.get(
"geometry_maximum_gap_seconds"
),
"geometry_maximum_centroid_distance_m": association.get(
"geometry_maximum_centroid_distance_m"
),
},
"metrics": {
"source_frames": frames.get("source"),
"processed_frames": frames.get("processed"),
"frames_with_current_layer": frames.get("with_current_layer"),
"held_only_frames": frames.get("held_only"),
"unknown_empty_frames": frames.get("unknown_empty"),
"created_components": components.get("created"),
"camera_created_components": components.get("camera_created"),
"geometry_created_components": components.get("geometry_created"),
"geometry_reassociated_components": components.get(
"geometry_reassociated_components"
),
"exact_camera_associations": components.get(
"exact_camera_associations"
),
"geometry_spatial_reassociations": components.get(
"geometry_spatial_reassociations"
),
"held_publications": components.get("held_publications"),
"expired_components": components.get("expired"),
"peak_active_components": components.get("peak_active"),
"maximum_observed_span_seconds": components.get(
"maximum_observed_span_seconds"
),
"continuity_fraction": components.get("continuity_fraction"),
"geometry_only_reassociation_fraction": components.get(
"geometry_only_reassociation_fraction"
),
"e32_current_point_rows": occupancy.get(
"e32_current_point_rows"
),
"e34_consumed_current_point_rows": occupancy.get(
"e34_consumed_current_point_rows"
),
"current_cell_rows": occupancy.get("current_cell_rows"),
"held_cell_rows": occupancy.get("held_cell_rows"),
"stored_cell_rows": occupancy.get("stored_cell_rows"),
"free_cell_rows": occupancy.get("free_cell_rows"),
"peak_cells_per_component": occupancy.get(
"peak_cells_per_component"
),
"maximum_held_age_seconds": aging.get(
"maximum_held_age_seconds"
),
"maximum_expiry_delay_seconds": aging.get(
"maximum_expiry_delay_seconds"
),
"maximum_replay_materialization_delay_seconds": aging.get(
"maximum_replay_materialization_delay_seconds"
),
"map_frame_jump_candidates": map_frame.get("jump_candidates"),
"frame_processing_p95_ms": frame_processing.get("p95"),
"build_elapsed_ms": runtime.get("build_elapsed_ms"),
},
"review": copy.deepcopy(result.review),
"acceptance": copy.deepcopy(acceptance),
"decision": copy.deepcopy(result.report.get("decision")),
"authority": copy.deepcopy(result.report.get("authority")),
"access": "read-only",
}
def _empty_catalog(configured: bool) -> dict[str, object]:
return {
"schema_version": LABORATORY_ADVANCED_CATALOG_SCHEMA,
@@ -310,6 +432,7 @@ def build_advanced_laboratory_router(
e31_root_provider: RootProvider = lambda: None,
e32_root_provider: RootProvider = lambda: None,
e33_root_provider: RootProvider = lambda: None,
e34_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
@@ -434,4 +557,39 @@ def build_advanced_laboratory_router(
"invalid_total": invalid_total,
}
@router.get("/e34/results")
def list_e34_results(
limit: int = Query(default=1, ge=1, le=10),
) -> dict[str, object]:
root = _configured_root(e34_root_provider)
if root is None:
return _empty_catalog(False)
candidates = _candidates(root, _E34_RESULT_ID)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in candidates:
try:
result = _read_e34_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
if not result.accepted:
raise ValueError("E34 result is not accepted")
if len(items) < limit:
items.append(_project_e34(result))
except (
E34TemporalOccupiedReplayError,
KeyError,
OSError,
TypeError,
ValueError,
):
invalid_total += 1
return {
**_empty_catalog(True),
"items": items,
"candidate_total": len(candidates),
"invalid_total": invalid_total,
}
return router
+7
View File
@@ -520,6 +520,13 @@ app.include_router(
/ "e33"
/ "results"
),
e34_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e34"
/ "results"
),
)
)
app.include_router(
+135 -3
View File
@@ -1,10 +1,13 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
from fastapi import APIRouter
from fastapi.routing import APIRoute
from pytest import MonkeyPatch
import k1link.web.advanced_laboratory_api as advanced_api
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
@@ -23,7 +26,7 @@ def _endpoint(router: APIRouter, path: str) -> object:
def test_advanced_catalogs_are_empty_when_not_configured() -> None:
router = build_advanced_laboratory_router()
for name in ("e31", "e32", "e33"):
for name in ("e31", "e32", "e33", "e34"):
route = _endpoint(router, f"/api/v1/laboratory/{name}/results")
catalog = route(limit=1) # type: ignore[operator]
assert catalog == {
@@ -42,18 +45,21 @@ def test_advanced_catalogs_fail_closed_on_incomplete_results(
e31 = tmp_path / "e31"
e32 = tmp_path / "e32"
e33 = tmp_path / "e33"
for root in (e31, e32, e33):
e34 = tmp_path / "e34"
for root in (e31, e32, e33, e34):
root.mkdir()
(e31 / f"e31-source-qualification-{'1' * 64}").mkdir()
(e32 / f"e32-track-geometry-{'2' * 64}").mkdir()
(e33 / f"e33-worker-shadow-{'3' * 64}").mkdir()
(e34 / f"e34-temporal-occupied-{'4' * 64}").mkdir()
router = build_advanced_laboratory_router(
e31_root_provider=lambda: e31,
e32_root_provider=lambda: e32,
e33_root_provider=lambda: e33,
e34_root_provider=lambda: e34,
)
for name in ("e31", "e32", "e33"):
for name in ("e31", "e32", "e33", "e34"):
route = _endpoint(router, f"/api/v1/laboratory/{name}/results")
catalog = route(limit=1) # type: ignore[operator]
assert catalog["configured"] is True
@@ -77,3 +83,129 @@ def test_advanced_catalog_does_not_follow_a_configured_symlink(
catalog = route(limit=1) # type: ignore[operator]
assert catalog["configured"] is False
def test_e34_catalog_projects_only_accepted_read_only_evidence(
tmp_path: Path,
monkeypatch: MonkeyPatch,
) -> None:
result_id = f"e34-temporal-occupied-{'4' * 64}"
root = tmp_path / "e34"
candidate = root / result_id
candidate.mkdir(parents=True)
(candidate / "manifest.json").write_text("{}", encoding="utf-8")
authority = {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
result = SimpleNamespace(
result_id=result_id,
accepted=True,
manifest={
"created_at_utc": "2026-07-27T13:28:30Z",
"acceptance_state": (
"accepted-bounded-occupied-unknown-temporal-layer"
),
"identity": {
"source_session_id": "source-session",
"e32_result_id": f"e32-track-geometry-{'2' * 64}",
"e33_result_id": f"e33-worker-shadow-{'3' * 64}",
"pipeline": "lidar-local-map/short-ttl-hit-only/v1",
"coordinate_frame": "map",
"profile": {
"profile_id": "e34-profile/v1",
"layer": {
"voxel_size_m": 0.45,
"occupied_ttl_seconds": 0.75,
"maximum_active_components": 256,
"maximum_cells_per_component": 4096,
},
"association": {
"geometry_maximum_gap_seconds": 0.35,
"geometry_maximum_centroid_distance_m": 0.9,
},
},
},
},
report={
"metrics": {
"frames": {
"source": 4489,
"processed": 4489,
"with_current_layer": 3928,
"held_only": 540,
"unknown_empty": 21,
},
"components": {
"created": 4912,
"camera_created": 1027,
"geometry_created": 3885,
"geometry_reassociated_components": 2366,
"exact_camera_associations": 5168,
"geometry_spatial_reassociations": 17433,
"held_publications": 39664,
"expired": 4895,
"peak_active": 37,
"maximum_observed_span_seconds": 13.394,
"continuity_fraction": 0.821466,
"geometry_only_reassociation_fraction": 0.81776,
},
"occupancy": {
"e32_current_point_rows": 2119302,
"e34_consumed_current_point_rows": 2119302,
"current_cell_rows": 834478,
"held_cell_rows": 531581,
"stored_cell_rows": 1366059,
"free_cell_rows": 0,
"peak_cells_per_component": 324,
},
"aging": {
"maximum_held_age_seconds": 0.75,
"maximum_expiry_delay_seconds": 0.0,
"maximum_replay_materialization_delay_seconds": 0.283,
},
"map_frame": {"jump_candidates": 0},
"runtime": {
"build_elapsed_ms": 4317.0,
"frame_processing_ms": {"p95": 1.483},
},
},
"acceptance": {"accepted": True, "requirements": {}},
"decision": {"next_gate": "A8/E35"},
"authority": authority,
},
review={
"schema_version": (
"missioncore.e34-temporal-review-timeline/v1"
),
"result_id": result_id,
"frames": [],
},
)
def fake_read(
root_text: str,
signature: tuple[int, ...],
) -> SimpleNamespace:
assert root_text == str(candidate.resolve())
assert signature
return result
monkeypatch.setattr(advanced_api, "_read_e34_cached", fake_read)
router = build_advanced_laboratory_router(
e34_root_provider=lambda: root,
)
route = _endpoint(router, "/api/v1/laboratory/e34/results")
catalog = route(limit=1) # type: ignore[operator]
assert catalog["candidate_total"] == 1
assert catalog["invalid_total"] == 0
item = catalog["items"][0]
assert item["status"] == (
"accepted-bounded-occupied-unknown-temporal-layer"
)
assert item["metrics"]["processed_frames"] == 4489
assert item["metrics"]["free_cell_rows"] == 0
assert item["authority"] == authority
assert item["access"] == "read-only"
+356
View File
@@ -0,0 +1,356 @@
from __future__ import annotations
import numpy as np
import pytest
from k1link.compute.temporal_occupied_layer import (
TemporalLayerConfig,
TemporalOccupiedLayer,
TemporalOccupiedLayerError,
)
from k1link.compute.track_geometry import (
PointSlab,
TrackGeometry,
TrackGeometryCurrentness,
TrackGeometryEvidenceState,
TrackGeometryFrame,
TrackGeometryMetricBasis,
TrackGeometryOwnerKind,
TrackGeometrySourceBinding,
)
def _config(**changes: object) -> TemporalLayerConfig:
values: dict[str, object] = {
"voxel_size_m": 0.45,
"occupied_ttl_seconds": 0.75,
"maximum_active_components": 256,
"maximum_cells_per_component": 4096,
"geometry_maximum_gap_seconds": 0.35,
"geometry_maximum_centroid_distance_m": 0.9,
"geometry_minimum_voxel_overlap_fraction": 0.05,
"geometry_neighbor_radius_cells": 1,
"jump_maximum_adjacent_gap_seconds": 0.25,
"jump_minimum_matched_components": 4,
"jump_minimum_median_displacement_m": 1.5,
"jump_minimum_p25_displacement_m": 0.9,
}
values.update(changes)
return TemporalLayerConfig(**values) # type: ignore[arg-type]
def _binding() -> TrackGeometrySourceBinding:
return TrackGeometrySourceBinding(
source_pack_id="e10-lidar-pack-" + "a" * 64,
source_session_id="source-session",
representation_profile_id="representation-profile/v1",
e31_qualification_id="e31-source-qualification-" + "b" * 64,
calibration_sha256="c" * 64,
coordinate_frame="map",
time_basis="source-time",
selected_offset_ms=0,
)
def _frame(
*,
frame_index: int,
seconds: float,
owners: list[
tuple[
str,
TrackGeometryOwnerKind,
list[list[float]],
int | None,
]
],
source_available: bool = True,
unknown_camera_owner: str | None = None,
) -> TrackGeometryFrame:
owner_keys = tuple(item[0] for item in owners)
source_indices: list[int] = []
points: list[list[float]] = []
owner_indices: list[int] = []
geometries: list[TrackGeometry] = []
for owner_index, (owner_key, owner_kind, owner_points, track_id) in enumerate(
owners
):
row_start = len(points)
points.extend(owner_points)
source_indices.extend(range(row_start, row_start + len(owner_points)))
owner_indices.extend([owner_index] * len(owner_points))
geometries.append(
TrackGeometry(
owner_key=owner_key,
owner_kind=owner_kind,
evidence_state=(
TrackGeometryEvidenceState.AGREE
if owner_kind is TrackGeometryOwnerKind.CAMERA_TRACK
else TrackGeometryEvidenceState.GEOMETRY_ONLY
),
currentness=TrackGeometryCurrentness.CURRENT,
metric_basis=TrackGeometryMetricBasis.CURRENT_POINTS,
reason_codes=("accepted-hit-backed-support",),
semantic_track_id=track_id,
semantic_label=(
"object"
if owner_kind is TrackGeometryOwnerKind.CAMERA_TRACK
else None
),
bbox_xyxy=(
(1.0, 1.0, 2.0, 2.0)
if owner_kind is TrackGeometryOwnerKind.CAMERA_TRACK
else None
),
range_m=1.0,
)
)
if unknown_camera_owner is not None:
track_id = int(unknown_camera_owner.split(":")[1])
geometries.append(
TrackGeometry(
owner_key=unknown_camera_owner,
owner_kind=TrackGeometryOwnerKind.CAMERA_TRACK,
evidence_state=TrackGeometryEvidenceState.CAMERA_ONLY,
currentness=TrackGeometryCurrentness.CURRENT,
metric_basis=TrackGeometryMetricBasis.UNAVAILABLE,
reason_codes=("camera-without-current-points",),
semantic_track_id=track_id,
semantic_label="object",
bbox_xyxy=(1.0, 1.0, 2.0, 2.0),
)
)
point_array = (
np.asarray(points, dtype="<f4")
if points
else np.empty((0, 3), dtype="<f4")
)
return TrackGeometryFrame(
binding=_binding(),
frame_index=frame_index,
source_frame_index=frame_index,
session_seconds=seconds,
source_available=source_available,
point_slab=PointSlab(
frame_index=frame_index,
source_frame_index=frame_index,
source_point_count=len(points),
coordinate_frame="map",
owner_keys=owner_keys,
source_indices=np.asarray(source_indices, dtype="<i8"),
points_xyz_m=point_array,
owner_indices=np.asarray(owner_indices, dtype="<u4"),
),
geometries=tuple(geometries),
)
def test_temporal_layer_holds_unknown_then_expires_without_publishing_cells() -> None:
layer = TemporalOccupiedLayer(_config())
current = layer.update(
_frame(
frame_index=0,
seconds=0.0,
owners=[
(
"track:7",
TrackGeometryOwnerKind.CAMERA_TRACK,
[[1.0, 2.0, 0.5], [1.1, 2.0, 0.5]],
7,
)
],
)
)
held = layer.update(
_frame(
frame_index=1,
seconds=0.5,
owners=[],
source_available=False,
)
)
expired = layer.update(
_frame(
frame_index=2,
seconds=0.9,
owners=[],
source_available=False,
)
)
assert current.document["current"][0]["occupancy_state"] == "occupied"
assert held.document["held"][0]["occupancy_state"] == "unknown"
assert held.document["held"][0]["cell_row_count"] > 0
assert expired.document["expired"][0]["occupancy_state"] == "unknown"
assert expired.document["expired"][0]["cell_row_count"] == 0
assert expired.document["expired"][0]["expiration_event"] == {
"deadline_seconds": 0.75,
"emitted_at_seconds": 0.75,
"emission_delay_seconds": 0.0,
"replay_materialized_at_seconds": 0.9,
"replay_materialization_delay_seconds": pytest.approx(0.15),
"clock": "independent-layer-deadline",
}
assert expired.cell_rows.shape == (0, 3)
assert layer.active_component_count == 0
def test_geometry_only_components_reassociate_without_inventing_semantics() -> None:
layer = TemporalOccupiedLayer(_config())
first = layer.update(
_frame(
frame_index=0,
seconds=0.0,
owners=[
(
"geometry:0",
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
[[1.0, 1.0, 0.5], [1.2, 1.0, 0.5]],
None,
)
],
)
)
second = layer.update(
_frame(
frame_index=1,
seconds=0.1,
owners=[
(
"geometry:9",
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
[[1.1, 1.0, 0.5], [1.3, 1.0, 0.5]],
None,
)
],
)
)
assert first.document["current"][0]["temporal_id"] == second.document[
"current"
][0]["temporal_id"]
assert second.document["current"][0]["association_reason"] == (
"geometry-spatial-reassociation"
)
assert second.document["current"][0]["semantic_provenance"] == {
"owner": None,
"track_ids": [],
"labels": [],
"class_inferred_by_e34": False,
}
def test_unknown_without_current_points_cannot_create_occupied_cells() -> None:
layer = TemporalOccupiedLayer(_config())
projection = layer.update(
_frame(
frame_index=0,
seconds=0.0,
owners=[],
unknown_camera_owner="track:11",
)
)
assert projection.document["layer_state"] == "unknown"
assert projection.document["current"] == []
assert projection.document["input"]["unknown_without_current_points"] == 1
assert projection.cell_rows.shape == (0, 3)
assert layer.active_component_count == 0
def test_temporal_layer_detects_supported_global_map_frame_jump() -> None:
layer = TemporalOccupiedLayer(_config())
first = [
(
f"track:{track_id}",
TrackGeometryOwnerKind.CAMERA_TRACK,
[[float(track_id), 0.0, 0.5]],
track_id,
)
for track_id in range(1, 5)
]
shifted = [
(
f"track:{track_id}",
TrackGeometryOwnerKind.CAMERA_TRACK,
[[float(track_id) + 2.0, 0.0, 0.5]],
track_id,
)
for track_id in range(1, 5)
]
layer.update(_frame(frame_index=0, seconds=0.0, owners=first))
projection = layer.update(
_frame(frame_index=1, seconds=0.1, owners=shifted)
)
assert projection.document["map_frame_jump"]["candidate"] is True
assert projection.document["map_frame_jump"][
"matched_component_count"
] == 4
def test_temporal_layer_rejects_incoherent_nearest_neighbor_jump() -> None:
layer = TemporalOccupiedLayer(_config())
first = [
(
f"geometry:{index}",
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
[[float(index) * 5.0, 0.0, 0.5]],
None,
)
for index in range(4)
]
incoherent = [
(
f"geometry:{index}",
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
[[x, y, 0.5]],
None,
)
for index, (x, y) in enumerate(
((2.0, 0.0), (8.0, 2.0), (13.0, -3.0), (25.0, 4.0))
)
]
layer.update(_frame(frame_index=0, seconds=0.0, owners=first))
projection = layer.update(
_frame(frame_index=1, seconds=0.1, owners=incoherent)
)
assert projection.document["map_frame_jump"]["candidate"] is False
assert projection.document["map_frame_jump"]["reason"] == (
"no-coherent-global-translation"
)
def test_temporal_layer_fails_closed_instead_of_truncating_cells() -> None:
layer = TemporalOccupiedLayer(_config(maximum_cells_per_component=1))
with pytest.raises(
TemporalOccupiedLayerError,
match="cell bound exceeded",
):
layer.update(
_frame(
frame_index=0,
seconds=0.0,
owners=[
(
"geometry:0",
TrackGeometryOwnerKind.GEOMETRY_CLUSTER,
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]],
None,
)
],
)
)
def test_temporal_layer_rejects_non_monotonic_replay() -> None:
layer = TemporalOccupiedLayer(_config())
layer.update(_frame(frame_index=0, seconds=0.0, owners=[]))
with pytest.raises(
TemporalOccupiedLayerError,
match="frame order changed",
):
layer.update(_frame(frame_index=2, seconds=0.1, owners=[]))