Author SHA1 Message Date
DCCONSTRUCTIONS b2a1b23131 fix(observatory): preserve camera across follow transitions 2026-09-05 08:46:35 +03:00
DCCONSTRUCTIONS cada687173 feat(observatory): ship modular AI inference labs 2026-09-04 17:59:05 +03:00
DCCONSTRUCTIONS eff60e490a docs(observatory): record saved replay evidence and open visual gates 2026-09-03 13:24:32 +03:00
DCCONSTRUCTIONS 9db29bcdc6 feat(observatory): share native result replay and expose saved TGS layers 2026-09-03 13:23:56 +03:00
DCCONSTRUCTIONS 27979854a7 feat(observatory): cache sealed camera and TGS replay on Core 2026-09-03 13:15:07 +03:00
DCCONSTRUCTIONS 3ea8d987a7 docs(observatory): record real M49 publication and restart acceptance 2026-09-03 11:50:32 +03:00
DCCONSTRUCTIONS bd947b49f4 fix(observatory): preserve source identity on unchanged reindex 2026-09-03 11:43:03 +03:00
DCCONSTRUCTIONS 54c8d82884 ops(observatory): install exact recorded-progress agent layers 2026-09-03 11:22:03 +03:00
DCCONSTRUCTIONS bee8552003 feat(observatory): expose attempt-bound recorded progress 2026-09-03 11:17:56 +03:00
DCCONSTRUCTIONS 4e0565eb4b docs(observatory): refresh four-stage recorded-first roadmap 2026-09-03 10:35:48 +03:00
DCCONSTRUCTIONS 44afadb021 docs(observatory): record selector rules and local UI acceptance 2026-09-03 10:16:12 +03:00
DCCONSTRUCTIONS e436fb56d4 fix(observatory): offer only uncalculated profiles without completion badges 2026-09-03 10:16:01 +03:00
DCCONSTRUCTIONS c2710b55a7 docs(observatory): record cache activation and Docker memory recovery 2026-09-03 09:44:02 +03:00
DCCONSTRUCTIONS d777f3198b docs(observatory): record cache proof and pending activation 2026-09-03 09:15:12 +03:00
DCCONSTRUCTIONS 1e4ddc2cff feat(observatory): verify exact published result reuse 2026-09-03 09:14:54 +03:00
DCCONSTRUCTIONS 80fc0058cb docs(observatory): record claim-v3 repair and preserved queue history 2026-09-03 00:00:08 +03:00
DCCONSTRUCTIONS 43f1cdc862 fix(worker): preserve transport patch in Docker snapshot layers 2026-09-02 23:50:22 +03:00
DCCONSTRUCTIONS 0041e9fadb fix(worker): bind claim migration to the imported transport module 2026-09-02 23:45:06 +03:00
DCCONSTRUCTIONS cabd3081c7 fix(observatory): version non-binding idle claims and preserve grant history 2026-09-02 23:40:11 +03:00
DCCONSTRUCTIONS 049ed3eed4 docs(observatory): prioritize recorded laboratories and cached review before onboard profiles 2026-09-02 23:16:01 +03:00
DCCONSTRUCTIONS 35c1249631 fix(observatory): reject duplicate active portable computations atomically 2026-09-02 23:15:39 +03:00
DCCONSTRUCTIONS 941183d624 docs(perception): record TLS and direct TCP latency attribution 2026-09-02 22:40:08 +03:00
DCCONSTRUCTIONS 6af4c208ce test(perception): localize transport tails with bounded TLS record witnesses 2026-09-02 22:34:12 +03:00
DCCONSTRUCTIONS 75a5320756 docs(perception): record clock route A/B/A and continuous LAN canary 2026-09-02 22:14:58 +03:00
DCCONSTRUCTIONS 59a74a3677 test(perception): add bounded clock-only route and scheduler diagnostics 2026-09-02 21:57:02 +03:00
DCCONSTRUCTIONS 35d28418f3 docs(perception): record joint startup proof and remaining clock wait 2026-09-02 21:16:03 +03:00
DCCONSTRUCTIONS 48835a0499 fix(perception): require joint clock readiness before source activation 2026-09-02 21:03:33 +03:00
DCCONSTRUCTIONS 93ae1edbb4 docs(perception): record cross-host canary failure and joint startup gate 2026-09-02 20:44:08 +03:00
DCCONSTRUCTIONS 093df8b38f style(perception): format streaming lifecycle guards 2026-09-02 20:43:31 +03:00
DCCONSTRUCTIONS 35b6cd9e9e feat(perception): gate cross-host graph with acknowledged source clocks 2026-09-02 20:37:21 +03:00
DCCONSTRUCTIONS d80df61a7e docs(perception): record cross-host clock and control proof 2026-09-02 19:46:27 +03:00
DCCONSTRUCTIONS d7b8989e43 feat(perception): add scoped stream control and bounded clock observations 2026-09-02 19:41:43 +03:00
DCCONSTRUCTIONS 0cc6b96306 docs(perception): record full-graph network and recovery evidence 2026-09-02 19:05:13 +03:00
DCCONSTRUCTIONS 6469505c41 feat(perception): connect full graph to external streaming source and scene receiver 2026-09-02 19:04:41 +03:00
DCCONSTRUCTIONS 191612282b test(perception): record cross-host gRPC recovery and backpressure proof 2026-09-02 18:14:51 +03:00
DCCONSTRUCTIONS 0310592ee3 feat(perception): add bounded authenticated gRPC stream transport 2026-09-02 18:11:42 +03:00
DCCONSTRUCTIONS df0158e1ba docs(perception): record Worker observability repair and remaining stream gates 2026-09-02 17:40:45 +03:00
DCCONSTRUCTIONS 2ef8c08bf2 feat(perception): export bounded runtime observations outside heartbeat lane 2026-09-02 17:35:46 +03:00
DCCONSTRUCTIONS c570f7d938 fix(telemetry): restore worker agent delivery and truthful system status 2026-09-02 17:35:20 +03:00
DCCONSTRUCTIONS 03e8e71bea docs(perception): record host telemetry recovery and scoped inventory evidence 2026-09-02 16:56:22 +03:00
DCCONSTRUCTIONS 61cbdb30a0 feat(perception): connect scoped host telemetry to recoverable profile lifecycle 2026-09-02 16:50:57 +03:00
DCCONSTRUCTIONS e91721fe6a docs(perception): record full graph recovery and separate availability latency 2026-09-02 16:01:36 +03:00
DCCONSTRUCTIONS 70927ea585 feat(perception): resume full graph on fresh input without restarting models 2026-09-02 15:54:13 +03:00
DCCONSTRUCTIONS 03a7e730ae docs(perception): record recoverable input policy and decoder continuity proof 2026-09-02 15:33:53 +03:00
DCCONSTRUCTIONS c31c46caa5 feat(perception): preserve resident runtime across recoverable input gaps 2026-09-02 15:27:06 +03:00
DCCONSTRUCTIONS 06a66a3da8 docs(perception): record controller readiness boundary and fault evidence 2026-09-02 14:54:43 +03:00
DCCONSTRUCTIONS 92625bf48b feat(perception): gate streaming lifecycle on controller readiness 2026-09-02 14:49:36 +03:00
DCCONSTRUCTIONS b8ba0cae1e docs(perception): record exact parity and fixed-envelope IPC comparison 2026-09-02 14:28:58 +03:00
DCCONSTRUCTIONS f39f1ff0b6 perf(perception): borrow contiguous image buffers for local IPC 2026-09-02 14:20:39 +03:00
DCCONSTRUCTIONS 380b6eaba7 fix(perception): preserve quaternion alignment across binary ingress 2026-09-02 14:20:38 +03:00
DCCONSTRUCTIONS f6d73d8bfb docs(perception): record binary full-graph evidence and remaining gates 2026-09-02 14:02:28 +03:00
DCCONSTRUCTIONS 221e429c0a feat(perception): connect binary ingress to the supervised full graph 2026-09-02 13:53:25 +03:00
DCCONSTRUCTIONS a85aed2e03 docs(perception): record decoder parity and remaining full-graph bridge 2026-09-02 13:14:11 +03:00
DCCONSTRUCTIONS f102712f0b test(perception): verify incremental decoder parity and binary ingress on Worker 2026-09-02 13:07:33 +03:00
DCCONSTRUCTIONS 350366bae0 refactor(perception): decode bounded fMP4 without source file dependencies 2026-09-02 13:07:33 +03:00
DCCONSTRUCTIONS c23e86fd39 docs(perception): record bounded binary ingress evidence and remaining bridge 2026-09-02 12:37:25 +03:00
DCCONSTRUCTIONS 79ce55d197 test(perception): verify reuse of existing raw live ingress 2026-09-02 12:30:16 +03:00
DCCONSTRUCTIONS a937400942 test(perception): seal source end marker before IPC shutdown 2026-09-02 12:28:23 +03:00
DCCONSTRUCTIONS da60feff90 feat(perception): add bounded binary stream ingress 2026-09-02 12:21:25 +03:00
DCCONSTRUCTIONS 85349f42f9 docs(perception): record lifecycle ownership and expiry evidence 2026-09-02 12:05:00 +03:00
DCCONSTRUCTIONS ac69e3b444 test(perception): exercise lifecycle and lease loss in full profile 2026-09-02 11:55:57 +03:00
DCCONSTRUCTIONS 6acf468b25 feat(perception): fence streaming lifecycle with worker-local ownership 2026-09-02 11:55:56 +03:00
DCCONSTRUCTIONS d264cbce04 docs(perception): record stage-two scheduler and cell freshness proof 2026-09-02 11:03:26 +03:00
DCCONSTRUCTIONS 1f8101e4a5 feat(perception): expire costmap permissions per cell 2026-09-02 10:57:42 +03:00
DCCONSTRUCTIONS bcacb02a22 refactor(perception): share bounded streaming scheduler 2026-09-02 10:57:41 +03:00
DCCONSTRUCTIONS adcca7aa81 docs(perception): close stage-one baseline and record freshness proof 2026-09-02 10:21:44 +03:00
DCCONSTRUCTIONS 097e450a87 feat(perception): enforce six-layer freshness through receipt 2026-09-02 10:14:28 +03:00
DCCONSTRUCTIONS 4e633a662c docs(perception): record gpu clock latency proof 2026-09-02 09:42:40 +03:00
DCCONSTRUCTIONS c6c42c29e8 fix(perception): reject material ties and trace gpu pacing 2026-09-02 09:34:20 +03:00
DCCONSTRUCTIONS ce09d30c1a docs(perception): record latency and triton parity limits 2026-09-02 02:52:38 +03:00
DCCONSTRUCTIONS 34f13ba59e feat(perception): add bounded triton runtime diagnostics 2026-09-02 02:52:11 +03:00
DCCONSTRUCTIONS d9ea7c1129 perf(perception): reuse local surface cell observations 2026-09-02 02:03:09 +03:00
DCCONSTRUCTIONS a3c67e249c docs(perception): record rendezvous worker proof 2026-09-02 01:50:35 +03:00
DCCONSTRUCTIONS 294585936f fix(perception): rendezvous completed gpu handoff 2026-09-02 01:48:25 +03:00
DCCONSTRUCTIONS 380a13688c docs(perception): record preroll worker pilot 2026-09-02 01:40:06 +03:00
DCCONSTRUCTIONS fd8313899b fix(perception): separate preroll history from current sensor binding
Preserve rolling points and original clocks; expose per-modality ages, held pose and rejection reasons. Saved-lineage audit changes only the first admission (75 to 76 of 128). No new model/performance claim. 384 focused Python and 62 frontend tests passed.
2026-09-02 01:09:03 +03:00
DCCONSTRUCTIONS 05c99efb68 test(observatory): isolate legacy contracts from installed package pins
Freeze configuration-only legacy fixtures from 7025e17, update current installed-image assertions, and explicitly reject the prior agent image. Production admission and digest validation are unchanged.
2026-09-02 01:02:38 +03:00
DCCONSTRUCTIONS ffd6be5606 docs(observatory): record realtime plan and measured latency evidence 2026-09-02 00:59:48 +03:00
DCCONSTRUCTIONS bfe6ef4c77 feat(perception): add bounded full-graph streaming and latency pilots 2026-09-02 00:59:48 +03:00
DCCONSTRUCTIONS 4e04d06191 feat(perception): define stream-first profile and qualification contracts 2026-09-02 00:59:47 +03:00
DCCONSTRUCTIONS 5a78c997ac feat(observatory-ui): review portable results and retry publication
Checkpoint the existing generic result-viewing and publication lifecycle UI. Focused architecture and Observatory tests: 62 passed; no new visual changes or rebuild in this checkpoint.
2026-09-02 00:59:21 +03:00
DCCONSTRUCTIONS 62d5520c7a feat(worker): package independent installed LAB container steps
Preserve local installer, offline validation and pinned component adapters; 62 focused packaging tests pass. No deployment performed by this commit.
2026-09-02 00:58:51 +03:00
DCCONSTRUCTIONS a945d665dd feat(observatory): add installed package dispatch and durable publication
Checkpoint existing backend lifecycle changes. Focused verification found nine legacy fixture failures in portable LAB V1 executor/runtime tests; repair follows separately without rewriting this snapshot. ADR date retains its intentional Markdown hard break.
2026-09-02 00:58:37 +03:00
DCCONSTRUCTIONS d655d6998d feat(observatory): attest recording equipment and capture profiles 2026-09-02 00:57:27 +03:00
DCCONSTRUCTIONS 7025e173a3 fix(worker): accept omitted empty image command 2026-08-31 21:47:47 +03:00
DCCONSTRUCTIONS 10fe727561 fix(worker): preserve exec-form entrypoint 2026-08-31 21:45:51 +03:00
DCCONSTRUCTIONS 00ee5f163a fix(worker): keep commit labels shell-safe 2026-08-31 21:42:31 +03:00
DCCONSTRUCTIONS cbcc09afd6 fix(worker): mount generated shell scripts 2026-08-31 21:39:07 +03:00
DCCONSTRUCTIONS e958729e9d fix(worker): seal coordinator Python dependencies 2026-08-31 21:31:45 +03:00
DCCONSTRUCTIONS 0fcd037e4f fix(worker): compose staged source paths safely 2026-08-31 21:01:30 +03:00
DCCONSTRUCTIONS 11a146011f feat(worker): install combined observatory profiles 2026-08-31 20:56:29 +03:00
DCCONSTRUCTIONS 44ebbe7ca2 test(lab): import adapters in legacy runtimes 2026-08-31 20:04:31 +03:00
DCCONSTRUCTIONS a3299dcfc3 fix(lab): avoid runtime typing on Python 3.9 2026-08-31 20:01:49 +03:00
DCCONSTRUCTIONS 2989fba99c fix(lab): support legacy Python 3.9 adapters 2026-08-31 19:58:20 +03:00
DCCONSTRUCTIONS 142481f0d8 fix(lab): probe optional adapter image safely 2026-08-31 19:52:34 +03:00
DCCONSTRUCTIONS b2ae29b8d8 feat(lab): install thin adapter layers offline 2026-08-31 19:51:51 +03:00
DCCONSTRUCTIONS 8eced6c34d feat(worker): compose installed LAB V1 profile 2026-08-31 19:47:24 +03:00
DCCONSTRUCTIONS 732cc139fd refactor(worker): externalize runtime registries 2026-08-31 19:47:07 +03:00
DCCONSTRUCTIONS 104e54f4dd refactor(lab): separate release contract identity 2026-08-31 19:46:58 +03:00
DCCONSTRUCTIONS 7b021782d3 feat(lab): install sealed component adapters 2026-08-31 19:46:47 +03:00
DCCONSTRUCTIONS 759830b054 feat(lab): seal reusable EoMT runtime trees 2026-08-31 19:24:42 +03:00
DCCONSTRUCTIONS 19c0bd70d0 feat(lab): bind sealed component adapter images 2026-08-31 19:19:47 +03:00
DCCONSTRUCTIONS 35d99e40d5 perf(m49): verify source payloads during assembly 2026-08-31 19:06:59 +03:00
DCCONSTRUCTIONS bfc1f1bbed feat(worker): define unified agent image artifact 2026-08-31 19:00:06 +03:00
DCCONSTRUCTIONS f41428f907 feat(lab): add sealed local component runners 2026-08-31 18:59:47 +03:00
DCCONSTRUCTIONS 3744d79ad3 feat(lab): seal reusable ffmpeg runtime asset 2026-08-31 18:59:37 +03:00
DCCONSTRUCTIONS b57c3e94da perf(m49): defer fresh stage revalidation 2026-08-31 18:42:31 +03:00
DCCONSTRUCTIONS 73a7d28065 perf(lidar): avoid duplicate replay decode during build 2026-08-31 18:35:12 +03:00
DCCONSTRUCTIONS f64fbb1fa6 feat(worker): compose all ready observatory profiles 2026-08-31 18:31:54 +03:00
DCCONSTRUCTIONS 1ff527b264 perf(observatory): avoid duplicate camera verification 2026-08-31 18:25:09 +03:00
DCCONSTRUCTIONS def8c71410 perf(observatory): bound source destination planning 2026-08-31 18:19:04 +03:00
DCCONSTRUCTIONS 7e59176581 fix(observatory): disable source artifact compression 2026-08-31 18:05:34 +03:00
DCCONSTRUCTIONS 4cd94b5805 perf(observatory): pack camera epoch source transfer 2026-08-31 17:59:18 +03:00
DCCONSTRUCTIONS 1766c0bdb2 feat(observatory): reconcile quarantined recorded jobs 2026-08-31 17:59:03 +03:00
DCCONSTRUCTIONS 56bfeaee7b fix(observatory): admit camera archive metadata overhead 2026-08-31 17:19:45 +03:00
DCCONSTRUCTIONS 892d0085e3 refactor(worker): defer optional visualization imports 2026-08-31 17:15:13 +03:00
DCCONSTRUCTIONS e2de8188ab feat(observatory): bridge local M49 worker container 2026-08-31 16:58:03 +03:00
DCCONSTRUCTIONS 9c69d81296 feat(observatory): install local M49 worker path 2026-08-31 16:51:10 +03:00
DCCONSTRUCTIONS 2f6e45bc96 fix(observatory): validate M49 fixture filtering 2026-08-31 16:12:45 +03:00
DCCONSTRUCTIONS 8a1c578e0e fix(observatory): admit committed M49 executor candidates 2026-08-31 16:06:23 +03:00
DCCONSTRUCTIONS 9beb534108 feat(observatory): add portable calculation profiles 2026-08-31 15:42:56 +03:00
DCCONSTRUCTIONS d1b75efcea fix(observatory): fence portable catalog refresh 2026-08-31 11:11:36 +03:00
DCCONSTRUCTIONS c9302c78c2 feat(observatory): add portable LAB V1 foundation 2026-08-31 11:02:34 +03:00
DCCONSTRUCTIONS d67e86176e feat(observatory): add durable recorded compute queue 2026-08-31 01:04:25 +03:00
DCCONSTRUCTIONS e72609120f fix(observatory): align setup typography 2026-08-30 23:31:09 +03:00
DCCONSTRUCTIONS 177be8869f feat(local-service): allow exact worktree migration 2026-08-30 23:23:36 +03:00
DCCONSTRUCTIONS 2a5763d3fb feat(observatory): seal blocked run preparations 2026-08-30 23:14:05 +03:00
DCCONSTRUCTIONS 8d5aeb0533 feat(observatory): add laboratory setup preflight 2026-08-30 22:21:58 +03:00
DCCONSTRUCTIONS be83283ee0 fix(observatory): refine catalog and replay UX 2026-08-30 20:36:12 +03:00
DCCONSTRUCTIONS 023151c186 feat(observatory): admit canonical recorded replay 2026-08-30 19:21:27 +03:00
DCCONSTRUCTIONS e6a9846167 feat(observatory): add bounded M5.1 session review 2026-08-30 17:29:39 +03:00
451 changed files with 128061 additions and 1329 deletions
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Синхронизированная запись</title>
<style>
html, body, #viewer { width: 100%; height: 100%; margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<div id="viewer"></div>
<script type="module" src="/src/components/rerun/isolatedRerunEntry.ts"></script>
</body>
</html>
+2
View File
@@ -806,6 +806,8 @@ export default function App() {
<StatusBadge tone="neutral">Offline evaluation</StatusBadge>
) : activeDefinition.kind === "lab-archive" ? (
laboratoryAnnotation.control
) : activeDefinition.kind === "observatory" ? (
<StatusBadge tone="neutral">Только наблюдение</StatusBadge>
) : activeDefinition.root === "system" ? (
<SystemWorkspaceSelector
value={activeDefinition.id}
@@ -1,4 +1,11 @@
import { useEffect, useRef, useState } from "react";
import { createIsolatedRerunHost } from "./rerun/isolatedRerunHost";
import { keepRecordedBlueprintSession } from "../core/observation/recordedBlueprintLifecycle";
import {
RECORDED_RERUN_ORBITAL_EYE,
RECORDED_RERUN_PLAN_EYE,
type RecordedRerunCameraEye,
} from "./rerun/recordedRerunCameraJournal";
import type { SceneSettings } from "../sceneSettings";
import {
@@ -138,6 +145,17 @@ export interface RerunViewportProps {
interface RerunBlueprintChannel {
endpointUrl: string;
cameraContract?: string | null;
appliedFollowTrajectory?: boolean | null;
pendingFollowCameraEye?: RecordedRerunCameraEye | null;
configureCameraJournal?: (
eye: RecordedRerunCameraEye,
spatialViewportStart: number,
) => void;
getCameraEye?: () => RecordedRerunCameraEye;
setCameraViewportStart?: (spatialViewportStart: number) => void;
getCurrentTimeNs?: () => number | null;
setCameraMaxOrbitalRadius?: (maxOrbitalRadius: number) => void;
channel: {
readonly ready: boolean;
send_rrd: (rrdBytes: Uint8Array) => void;
@@ -166,6 +184,13 @@ interface RecordedRerunIdentity {
const RECORDED_RRD_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/recording\.rrd$/;
const LAB_RECORDED_REPLAY_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-replay\.rrd$/;
const PORTABLE_RECORDED_REPLAY_PATH = /^\/api\/v1\/observatory\/portable-results\/(?:m49-tgs-portable-review|lab-v1-eomt-ddrnet|ai-layer-(?:ddrnet|eomt|rf-detr|object-distance))-[a-f0-9]{64}\/replays\/[a-f0-9]{64}\/recording\.rrd$/;
const COMPOSITION_RECORDED_REPLAY_PATH = /^\/api\/v1\/observatory\/ai-composition-runs\/ai-composition-[a-f0-9]{64}\/replays\/[a-f0-9]{64}\/recording\.rrd$/;
export function isRecordedRrdSource(path: string): boolean {
return RECORDED_RRD_PATH.test(path) || LAB_RECORDED_REPLAY_PATH.test(path)
|| PORTABLE_RECORDED_REPLAY_PATH.test(path) || COMPOSITION_RECORDED_REPLAY_PATH.test(path);
}
const RECORDED_BLUEPRINT_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/blueprint\.rrd$/;
const RECORDED_PERCEPTION_PATH = /^\/api\/v1\/observation-sessions\/[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\/perception\.rrd$/;
const LAB_RECORDED_PERCEPTION_PATH = /^\/api\/v1\/laboratory\/vegetation-shadow\/lab-v1-vegetation-shadow-[a-f0-9]{64}\/canonical-overlay\.rrd$/;
@@ -230,8 +255,7 @@ export function resolveRecordedViewerSourceUrl(
const expectedViewerSourceUrl = `${descriptor.sourceUrl}?generation=${descriptor.sha256}`;
const endpoint = new URL(descriptor.viewerSourceUrl, `${base.origin}/`);
if (
!(RECORDED_RRD_PATH.test(descriptor.sourceUrl)
|| LAB_RECORDED_REPLAY_PATH.test(descriptor.sourceUrl)) ||
!isRecordedRrdSource(descriptor.sourceUrl) ||
descriptor.viewerSourceUrl !== expectedViewerSourceUrl ||
endpoint.origin !== base.origin ||
endpoint.pathname !== descriptor.sourceUrl ||
@@ -262,7 +286,9 @@ export function resolveRecordedBlueprintUrl(
if (explicitSourceUrl !== undefined) {
const explicit = explicitSourceUrl.trim();
if (
!LAB_RECORDED_REPLAY_PATH.test(normalized)
!(LAB_RECORDED_REPLAY_PATH.test(normalized)
|| PORTABLE_RECORDED_REPLAY_PATH.test(normalized)
|| COMPOSITION_RECORDED_REPLAY_PATH.test(normalized))
|| !RECORDED_BLUEPRINT_PATH.test(explicit)
) return null;
const endpoint = new URL(explicit, `${base.origin}/`);
@@ -388,7 +414,13 @@ export async function fetchRecordedBlueprintRrd(
followTrajectory = false,
semanticLayer,
unifiedPerception,
unifiedCameraShare = 0.46,
planView = false,
cameraEye,
eyeRelativeToTracking = false,
currentTimeNs,
reactivateUpdates = false,
onCameraMaxOrbitalRadius,
perceptionLayers = {
enabled: false,
detections2d: false,
@@ -405,7 +437,13 @@ export async function fetchRecordedBlueprintRrd(
followTrajectory?: boolean;
semanticLayer?: "city" | "vegetation";
unifiedPerception?: boolean;
unifiedCameraShare?: number;
planView?: boolean;
cameraEye?: RecordedRerunCameraEye;
eyeRelativeToTracking?: boolean;
currentTimeNs?: number | null;
reactivateUpdates?: boolean;
onCameraMaxOrbitalRadius?: (maxOrbitalRadius: number) => void;
perceptionLayers?: RecordedPerceptionLayers;
fetcher?: typeof globalThis.fetch;
},
@@ -421,10 +459,8 @@ export async function fetchRecordedBlueprintRrd(
!RECORDED_BLUEPRINT_PATH.test(endpoint.pathname) ||
!Number.isFinite(settings.accumulationSeconds) ||
settings.accumulationSeconds < 0 ||
settings.accumulationSeconds > 3600 ||
!Number.isFinite(settings.pointSize) ||
settings.pointSize < 0.1 ||
settings.pointSize > 32 ||
!["intensity", "height", "distance", "rgb", "class"].includes(settings.colorMode) ||
!["turbo", "viridis", "plasma", "grayscale", "custom"].includes(settings.palette) ||
!/^#[0-9A-Fa-f]{6}$/.test(settings.customColor) ||
@@ -432,12 +468,28 @@ export async function fetchRecordedBlueprintRrd(
![0, 1].includes(viewResetGeneration) ||
(semanticLayer !== undefined && !["city", "vegetation"].includes(semanticLayer)) ||
typeof resolvedUnifiedPerception !== "boolean" ||
!Number.isFinite(unifiedCameraShare) ||
unifiedCameraShare < 0.1 ||
unifiedCameraShare > 0.9 ||
typeof planView !== "boolean" ||
typeof eyeRelativeToTracking !== "boolean" ||
typeof reactivateUpdates !== "boolean" ||
(eyeRelativeToTracking && (cameraEye === undefined || currentTimeNs == null)) ||
(cameraEye !== undefined && [
...cameraEye.position,
...cameraEye.lookTarget,
...cameraEye.eyeUp,
].some((value) => !Number.isFinite(value))) ||
(currentTimeNs !== undefined && currentTimeNs !== null && (
!Number.isSafeInteger(currentTimeNs) || currentTimeNs < 0
)) ||
[
perceptionLayers.enabled,
perceptionLayers.cameraImage ?? true,
perceptionLayers.detections2d,
perceptionLayers.segmentation,
perceptionLayers.cuboids3d,
perceptionLayers.costmap ?? false,
].some((value) => typeof value !== "boolean") ||
identity.applicationId !== "nodedc_mission_core_recorded" ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId) ||
@@ -468,11 +520,27 @@ export async function fetchRecordedBlueprintRrd(
view_reset_generation: viewResetGeneration,
follow_trajectory: followTrajectory,
unified_perception: resolvedUnifiedPerception,
unified_camera_share: unifiedCameraShare,
semantic_layer: semanticLayer ?? null,
plan_view: planView,
eye_position: cameraEye?.position ?? null,
eye_look_target: cameraEye?.lookTarget ?? null,
eye_up: cameraEye?.eyeUp ?? null,
...(eyeRelativeToTracking ? { eye_relative_to_tracking: true } : {}),
...(currentTimeNs === undefined || currentTimeNs === null ? {} : {
current_time_ns: currentTimeNs,
}),
show_detections_2d: perceptionLayers.detections2d,
show_camera_image: perceptionLayers.cameraImage ?? true,
show_segmentation: perceptionLayers.segmentation,
show_cuboids_3d: perceptionLayers.cuboids3d,
...(perceptionLayers.costmap === undefined ? {} : {
show_costmap: perceptionLayers.costmap,
reactivate_updates: true,
}),
...(reactivateUpdates && perceptionLayers.costmap === undefined
? { reactivate_updates: true }
: {}),
}),
signal,
});
@@ -496,9 +564,34 @@ export async function fetchRecordedBlueprintRrd(
) {
throw new Error("Invalid recorded blueprint RRD");
}
const maxOrbitalRadius = Number(
response.headers.get("X-MissionCore-Camera-Max-Orbital-Radius"),
);
if (Number.isFinite(maxOrbitalRadius) && maxOrbitalRadius >= 0.02) {
onCameraMaxOrbitalRadius?.(maxOrbitalRadius);
}
return payload;
}
export function recordedCameraJournalContract(
{
activeView,
viewResetGeneration,
planView,
}: {
activeView: RecordedRerunView;
viewResetGeneration: 0 | 1;
planView: boolean;
followTrajectory: boolean;
},
): string {
// Following is a tracking property of the current native eye. It must never
// initialize the browser journal again: doing so replaces the operator's
// current pose with the startup preset immediately before the blueprint is
// sent. Plan/3D and explicit reset are the only preset transitions.
return [activeView, viewResetGeneration, planView].join(":");
}
export async function fetchRecordedPerceptionRrd(
endpointUrl: string,
identity: RecordedRerunIdentity,
@@ -707,6 +800,7 @@ export function RerunViewport({
const recordedPerceptionSourceUrl = recordedProfile?.perceptionSourceUrl;
const recordedSemanticLayer = recordedProfile?.semanticLayer;
const recordedUnifiedPerception = recordedProfile?.unifiedPerception ?? false;
const recordedUnifiedCameraShare = recordedProfile?.unifiedCameraShare ?? 0.46;
const recordedPlanView = recordedProfile?.planView ?? false;
const recordedPerceptionRetryGeneration =
recordedProfile?.perceptionRetryGeneration ?? 0;
@@ -752,9 +846,9 @@ export function RerunViewport({
: sourceUrl
? resolveRecordedPerceptionUrl(sourceUrl, window.location.origin)
: null;
const recordedPointColorsUrl = sourceUrl
? resolveRecordedPointColorsUrl(sourceUrl, window.location.origin)
: null;
const recordedPointColorsUrl = recordedBlueprintUrl
? recordedBlueprintUrl.replace(/\/blueprint\.rrd$/, "/point-colors.rrd")
: sourceUrl ? resolveRecordedPointColorsUrl(sourceUrl, window.location.origin) : null;
const presentationStatus = rerunPresentationStatus(
status,
presentationGate,
@@ -791,8 +885,7 @@ export function RerunViewport({
return;
}
const isRecordedSource = RECORDED_RRD_PATH.test(normalizedSource)
|| LAB_RECORDED_REPLAY_PATH.test(normalizedSource);
const isRecordedSource = isRecordedRrdSource(normalizedSource);
let resolvedSource: string;
try {
if (isRecordedSource) {
@@ -827,6 +920,9 @@ export function RerunViewport({
diagnosticLifecycle.verifyBuild();
let disposed = false;
const blueprintOwnerId = crypto.randomUUID().replaceAll("-", "");
blueprintSessionIdRef.current = blueprintOwnerId;
let releaseBlueprintSession = () => {};
let disposeViewer: (() => void) | undefined;
let recordedOpenWatchdog: {
arm: () => void;
@@ -1073,6 +1169,11 @@ export function RerunViewport({
);
}
activeViewerLifecycleRef.current = disposeActiveViewerLifecycle;
const restoreAfterPageCache = (event: PageTransitionEvent) => {
if (event.persisted) setRetryNonce((nonce) => nonce + 1);
};
window.addEventListener("pagehide", disposeActiveViewerLifecycle);
window.addEventListener("pageshow", restoreAfterPageCache);
host.replaceChildren();
appliedPointColorKeyRef.current = null;
@@ -1080,15 +1181,22 @@ export function RerunViewport({
setRecordingBufferProgress(null);
onStatusChange?.("loading");
void import("@rerun-io/web-viewer")
.then(async ({ WebViewer }) => {
const isolatedHost = isRecordedSource ? createIsolatedRerunHost(host) : null;
disposeViewer = () => isolatedHost?.dispose();
const nativeViewer = isolatedHost?.ready ?? import("@rerun-io/web-viewer")
.then(({ WebViewer }) => ({ viewer: new WebViewer(), mount: host }));
void nativeViewer
.then(async ({ viewer, mount }) => {
// React StrictMode intentionally disposes the first effect while the
// dynamic import is still pending. Never start that stale viewer (the
// vendor API treats a missing host as document.body).
if (disposed) return;
const viewer = new WebViewer();
if (disposed) {
viewer.stop();
isolatedHost?.dispose();
return;
}
disposeViewer = createReentrantViewerDisposer(() => {
releaseBlueprintSession();
clearRecordingTimers();
clearPlaybackRangeTimer();
playbackTimeUpdates.cancel();
@@ -1146,6 +1254,7 @@ export function RerunViewport({
// A partially initialized WASM handle can already be gone after a
// startup failure.
}
isolatedHost?.dispose();
host.replaceChildren();
});
if (isRecordedSource && recordedArtifact) {
@@ -1182,6 +1291,13 @@ export function RerunViewport({
event.application_id === "nodedc_mission_core_recorded" &&
/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(event.recording_id)
) {
releaseBlueprintSession = keepRecordedBlueprintSession({
endpointUrl: recordedBlueprintUrl,
origin: window.location.origin,
applicationId: event.application_id,
recordingId: event.recording_id,
ownerId: blueprintOwnerId,
});
const current = recordedIdentityRef.current;
if (
current?.applicationId !== event.application_id ||
@@ -1491,7 +1607,7 @@ export function RerunViewport({
};
await viewer.start(
rerunViewerInitialSource(resolvedSource),
host,
mount,
viewerOptions,
);
if (disposed) {
@@ -1509,7 +1625,36 @@ export function RerunViewport({
if (recordedBlueprintUrl) {
const channel = viewer.open_channel("missioncore/recorded-blueprint");
blueprintChannel = { endpointUrl: recordedBlueprintUrl, channel };
blueprintChannel = {
endpointUrl: recordedBlueprintUrl,
cameraContract: null,
appliedFollowTrajectory: null,
pendingFollowCameraEye: null,
configureCameraJournal: (eye, spatialViewportStart) => {
if ("configure_camera_journal" in viewer) {
viewer.configure_camera_journal(eye, spatialViewportStart);
}
},
getCameraEye: () => "get_camera_eye" in viewer
? viewer.get_camera_eye()
: RECORDED_RERUN_ORBITAL_EYE,
setCameraViewportStart: (spatialViewportStart) => {
if ("set_camera_viewport_start" in viewer) {
viewer.set_camera_viewport_start(spatialViewportStart);
}
},
getCurrentTimeNs: () => {
const currentIdentity = recordedIdentityRef.current;
if (!currentIdentity) return null;
return viewer.get_current_time(currentIdentity.recordingId, "session_time");
},
setCameraMaxOrbitalRadius: (maxOrbitalRadius) => {
if ("set_camera_max_orbital_radius" in viewer) {
viewer.set_camera_max_orbital_radius(maxOrbitalRadius);
}
},
channel,
};
blueprintChannelRef.current = blueprintChannel;
setBlueprintChannelRevision((revision) => revision + 1);
}
@@ -1569,6 +1714,8 @@ export function RerunViewport({
});
return () => {
window.removeEventListener("pagehide", disposeActiveViewerLifecycle);
window.removeEventListener("pageshow", restoreAfterPageCache);
if (activeViewerLifecycleRef.current === disposeActiveViewerLifecycle) {
activeViewerLifecycleRef.current = null;
}
@@ -1938,6 +2085,9 @@ export function RerunViewport({
useEffect(() => {
if (!recordedBlueprintUrl || !sceneSettings) return;
// Initialize the portable following-eye blueprint after native admission
// and its initial seek, not while the base RRD is still arriving.
if (recordedPerceptionLayers.costmap !== undefined && status !== "ready") return;
const active = blueprintChannelRef.current;
const identity = recordedIdentityRef.current;
if (
@@ -1946,29 +2096,116 @@ export function RerunViewport({
active.endpointUrl !== recordedBlueprintUrl ||
!active.channel.ready
) return;
const cameraContract = recordedCameraJournalContract(
{
activeView: recordedView,
viewResetGeneration: recordedViewResetGeneration,
planView: recordedPlanView,
followTrajectory: recordedFollowTrajectory,
},
);
const cameraContractChanged = active.cameraContract !== cameraContract;
if (cameraContractChanged) {
active.configureCameraJournal?.(
recordedPlanView ? RECORDED_RERUN_PLAN_EYE : RECORDED_RERUN_ORBITAL_EYE,
recordedUnifiedPerception ? recordedUnifiedCameraShare : 0,
);
}
const abort = new AbortController();
void fetchRecordedBlueprintRrd(recordedBlueprintUrl, sceneSettings, identity, {
origin: window.location.origin,
blueprintSessionId: blueprintSessionIdRef.current,
signal: abort.signal,
activeView: recordedView,
viewResetGeneration: recordedViewResetGeneration,
followTrajectory: recordedFollowTrajectory,
perceptionLayers: recordedPerceptionLayers,
semanticLayer: recordedSemanticLayer,
unifiedPerception: recordedUnifiedPerception,
planView: recordedPlanView,
}).then((payload) => {
if (
abort.signal.aborted ||
blueprintChannelRef.current !== active ||
recordedIdentityRef.current !== identity ||
!active.channel.ready
) {
const previousFollow = active.appliedFollowTrajectory ?? false;
const enablingFollow = recordedFollowTrajectory && !previousFollow;
const disablingFollow = !recordedFollowTrajectory && previousFollow;
const transitionEye = (enablingFollow || disablingFollow)
? active.getCameraEye?.()
: null;
if (enablingFollow && transitionEye) {
active.pendingFollowCameraEye = transitionEye;
} else if (disablingFollow) {
active.pendingFollowCameraEye = null;
}
const pendingFollowEye = recordedFollowTrajectory
? active.pendingFollowCameraEye ?? null
: null;
const requestBlueprint = async (
cameraEye?: RecordedRerunCameraEye,
eyeRelativeToTracking = false,
reactivateUpdates = false,
) => {
const currentTimeNs = active.getCurrentTimeNs?.();
if (eyeRelativeToTracking && currentTimeNs == null) {
throw new Error("Recorded tracking cursor is unavailable");
}
return fetchRecordedBlueprintRrd(recordedBlueprintUrl, sceneSettings, identity, {
origin: window.location.origin,
blueprintSessionId: blueprintSessionIdRef.current,
signal: abort.signal,
activeView: recordedView,
viewResetGeneration: recordedViewResetGeneration,
followTrajectory: recordedFollowTrajectory,
perceptionLayers: recordedPerceptionLayers,
semanticLayer: recordedSemanticLayer,
unifiedPerception: recordedUnifiedPerception,
unifiedCameraShare: recordedUnifiedCameraShare,
planView: recordedPlanView,
cameraEye,
eyeRelativeToTracking,
currentTimeNs,
reactivateUpdates,
onCameraMaxOrbitalRadius: (maxOrbitalRadius) => {
if (blueprintChannelRef.current === active) {
active.setCameraMaxOrbitalRadius?.(maxOrbitalRadius);
}
},
});
};
const canApply = () => (
!abort.signal.aborted &&
blueprintChannelRef.current === active &&
recordedIdentityRef.current === identity &&
active.channel.ready
);
const applyPayload = (payload: Uint8Array) => {
if (!canApply()) return false;
active.channel.send_rrd(payload);
active.setCameraViewportStart?.(
recordedUnifiedPerception ? recordedUnifiedCameraShare : 0,
);
return true;
};
void (async () => {
const firstEye = disablingFollow
? transitionEye ?? undefined
: !enablingFollow && (cameraContractChanged || pendingFollowEye)
? pendingFollowEye ?? active.getCameraEye?.()
: undefined;
const firstEyeIsTrackingRelative = Boolean(firstEye) && (
disablingFollow || recordedFollowTrajectory
);
const firstPayload = await requestBlueprint(
firstEye,
firstEyeIsTrackingRelative,
enablingFollow || disablingFollow || Boolean(pendingFollowEye),
);
if (!applyPayload(firstPayload)) return;
active.cameraContract = cameraContract;
active.appliedFollowTrajectory = recordedFollowTrajectory;
if (!enablingFollow || !transitionEye) {
if (pendingFollowEye && firstEye === pendingFollowEye) {
active.pendingFollowCameraEye = null;
}
return;
}
active.channel.send_rrd(payload);
}).catch(() => {
// Rerun 0.36.3 intentionally fits a newly tracked transform to its
// bounding box. Apply the operator's relative eye on the next native
// frame, after tracking is established, to retain direction and zoom.
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
if (!canApply()) return;
const stabilizedPayload = await requestBlueprint(transitionEye, true, true);
if (!applyPayload(stabilizedPayload)) return;
active.pendingFollowCameraEye = null;
})().catch(() => {
// The recording remains usable with its embedded default blueprint.
// A later settings change retries through the same small channel.
});
@@ -1981,11 +2218,15 @@ export function RerunViewport({
recordedFollowTrajectory,
recordedSemanticLayer,
recordedUnifiedPerception,
recordedUnifiedCameraShare,
recordedPlanView,
recordedPerceptionLayers.enabled,
recordedPerceptionLayers.cameraImage,
recordedPerceptionLayers.detections2d,
recordedPerceptionLayers.segmentation,
recordedPerceptionLayers.cuboids3d,
recordedPerceptionLayers.costmap,
status,
sceneSettings?.accumulationSeconds,
sceneSettings?.customColor,
sceneSettings?.colorMode,
@@ -0,0 +1,33 @@
import { CanonicalResultRerunReplay } from "./CanonicalResultRerunReplay";
import { resolveAICompositionReplay } from "../../core/laboratory/canonicalLabReplay";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import type { AICompositionRun } from "../../core/observatory/aiComposition";
function resolveComposition(
runId: string,
launch: ObservationSessionReplayLaunch,
options: { signal: AbortSignal },
) {
return resolveAICompositionReplay(runId, launch, options);
}
export function AICompositionReplay({ run }: { run: AICompositionRun }) {
const semantics = run.moduleIds.includes("ddrnet") || run.moduleIds.includes("eomt");
const detections = run.moduleIds.includes("rf-detr")
|| run.moduleIds.includes("object-distance");
const costmap = run.moduleIds.includes("tgs");
return (
<CanonicalResultRerunReplay
key={run.runId}
resultId={run.runId}
sessionId={run.sourceSessionId}
resolveReplay={resolveComposition}
semantics={semantics}
detections={detections}
costmap={costmap}
moduleIds={run.moduleIds}
viewerLayers={run.presentation.viewerLayers}
label={run.configurationLabel}
/>
);
}
@@ -88,6 +88,9 @@ export function CanonicalRecordedLabReplay<
mediaLayerControls,
spatialLayerControls,
spatialLeadingControl,
spatialTrailingControl,
mediaModeControlsVisible = true,
paneToolbarsAlwaysVisible = false,
mediaMultiLayer = false,
mediaContent,
spatialContent,
@@ -116,10 +119,14 @@ export function CanonicalRecordedLabReplay<
mediaLayerControls?: ReactNode;
spatialLayerControls?: ReactNode;
spatialLeadingControl?: ReactNode;
spatialTrailingControl?: ReactNode;
mediaModeControlsVisible?: boolean;
/** Keeps renderer-local controls inside their viewport when only one pane is present. */
paneToolbarsAlwaysVisible?: boolean;
mediaMultiLayer?: boolean;
mediaContent?: ReactNode;
spatialContent?: ReactNode;
/** One upstream Rerun viewer owns both panes and the shared playback clock. */
/** One upstream Rerun viewer owns both panes; this controlled split owns their shared geometry. */
unifiedContent?: ReactNode;
emptyMessage: string;
deckOverlays?: ReactNode;
@@ -162,14 +169,14 @@ export function CanonicalRecordedLabReplay<
aria-label={mediaAriaLabel}
hidden={mediaMode === "none"}
>
{splitView ? (
{splitView || paneToolbarsAlwaysVisible ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="media"
data-multi-semantic={mediaMultiLayer ? "true" : undefined}
>
{mediaLayerControls}
{mediaModeControls}
{mediaModeControlsVisible ? mediaModeControls : null}
</div>
) : null}
{mediaContent}
@@ -181,7 +188,7 @@ export function CanonicalRecordedLabReplay<
data-pane="spatial"
aria-label={spatialAriaLabel}
>
{splitView ? (
{splitView || paneToolbarsAlwaysVisible ? (
<div
className="m4-replay-threat-visual__pane-toolbar"
data-pane-toolbar="spatial"
@@ -190,6 +197,7 @@ export function CanonicalRecordedLabReplay<
<div className="m4-replay-threat-visual__spatial-toolbar-end">
{spatialLayerControls}
{spatialModeControls}
{spatialTrailingControl}
</div>
</div>
) : null}
@@ -216,7 +224,7 @@ export function CanonicalRecordedLabReplay<
expanded={expanded}
onModeChange={onMediaModeChange}
onExpandedChange={onExpandedChange}
modeControlsVisible={!splitView}
modeControlsVisible={!splitView && !paneToolbarsAlwaysVisible}
actions={actions}
overlay={overlay}
transport={transport}
@@ -0,0 +1,524 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
ActivityIndicator,
Button,
Checker,
ControlRow,
Icon,
IconButton,
Inspector,
InspectorSelectField,
RangeControl,
ToastStack,
Window,
type ToastItem,
} from "@nodedc/ui-react";
import { ObservationTimeline } from "../ObservationTimeline";
import {
RerunViewport,
isRecordedPlaybackPresentationReady,
type RerunPlaybackController,
type RerunPlaybackState,
type RerunViewportStatus,
} from "../RerunViewport";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../laboratory/CanonicalRecordedLabReplay";
import type { CanonicalLabReplayDescriptor } from "../../core/laboratory/canonicalLabReplay";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import type { AIViewerLayer } from "../../core/observatory/aiComposition";
import {
fetchLabViewProfile,
saveLabViewProfile,
} from "../../core/observatory/labViewProfile";
import {
defaultSceneSettings,
type PointColorMode,
type PointPalette,
type SceneSettings,
} from "../../sceneSettings";
type MediaMode = "camera";
type SpatialMode = "3d" | "plan";
interface CanonicalReplayLaunch {
base: ObservationSessionReplayLaunch;
replay: CanonicalLabReplayDescriptor;
}
const RERUN_UNIFIED_CAMERA_SHARE_PERCENT = 46;
const LAB_SCENE_SETTINGS_STORAGE_PREFIX = "missioncore.observatory.lab-scene-settings.v1:";
const LAB_VIEW_PROFILE_ERROR_TOAST = "observatory-lab-view-profile-error";
const COLOR_OPTIONS: readonly { value: PointColorMode; label: string }[] = [
{ value: "intensity", label: "Интенсивность" },
{ value: "height", label: "Высота Z" },
{ value: "distance", label: "Дистанция" },
{ value: "rgb", label: "RGB" },
{ value: "class", label: "Класс" },
];
const PALETTE_OPTIONS: readonly { value: PointPalette; label: string }[] = [
{ value: "turbo", label: "Turbo" },
{ value: "viridis", label: "Viridis" },
{ value: "plasma", label: "Plasma" },
{ value: "grayscale", label: "Серый" },
];
const LAB_SCENE_DEFAULTS: SceneSettings = {
...defaultSceneSettings,
pointSize: 3.8,
accumulationSeconds: 5,
};
export function normalizeLabSceneSettings(settings: SceneSettings): SceneSettings {
const pointSize = Number.isFinite(settings.pointSize) ? settings.pointSize : LAB_SCENE_DEFAULTS.pointSize;
const accumulationSeconds = Number.isFinite(settings.accumulationSeconds)
? settings.accumulationSeconds : LAB_SCENE_DEFAULTS.accumulationSeconds;
return {
...settings,
pointSize: Math.round(Math.max(0.1, pointSize) * 10) / 10,
accumulationSeconds: Math.round(Math.max(0, accumulationSeconds)),
};
}
function loadLabSceneSettings(resultId: string): SceneSettings {
if (typeof window === "undefined") return LAB_SCENE_DEFAULTS;
try {
const raw = window.localStorage.getItem(`${LAB_SCENE_SETTINGS_STORAGE_PREFIX}${resultId}`);
if (raw === null) return LAB_SCENE_DEFAULTS;
const value: unknown = JSON.parse(raw);
if (typeof value !== "object" || value === null || Array.isArray(value)) return LAB_SCENE_DEFAULTS;
const row = value as Record<string, unknown>;
const pointSize = row.pointSize;
const accumulationSeconds = row.accumulationSeconds;
const colorMode = row.colorMode;
const palette = row.palette;
const showGrid = row.showGrid;
const showLabels = row.showLabels;
const showCameraFrustums = row.showCameraFrustums;
if (
typeof pointSize !== "number" || !Number.isFinite(pointSize) || pointSize < 0.1
|| typeof accumulationSeconds !== "number" || !Number.isFinite(accumulationSeconds)
|| accumulationSeconds < 0
|| !COLOR_OPTIONS.some((option) => option.value === colorMode)
|| !PALETTE_OPTIONS.some((option) => option.value === palette)
|| typeof showGrid !== "boolean"
|| typeof showLabels !== "boolean"
|| typeof showCameraFrustums !== "boolean"
) return LAB_SCENE_DEFAULTS;
return {
...LAB_SCENE_DEFAULTS,
pointSize,
accumulationSeconds,
colorMode: colorMode as PointColorMode,
palette: palette as PointPalette,
showGrid,
showLabels,
showCameraFrustums,
};
} catch {
return LAB_SCENE_DEFAULTS;
}
}
export function CanonicalResultRerunReplay({
resultId,
sessionId,
initialPlaybackStartSeconds,
resolveReplay,
semantics = false,
detections = false,
costmap = false,
moduleIds = [],
viewerLayers = [],
label = "Сохранённый результат",
}: {
resultId: string;
sessionId: string;
initialPlaybackStartSeconds?: number;
resolveReplay: (resultId: string, launch: ObservationSessionReplayLaunch, options: { signal: AbortSignal }) => Promise<CanonicalLabReplayDescriptor>;
semantics?: boolean;
detections?: boolean;
costmap?: boolean;
portable?: boolean;
moduleIds?: readonly string[];
viewerLayers?: readonly AIViewerLayer[];
label?: string;
}) {
const {
mediaMode,
spatialMode,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange,
onExpandedChange,
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
initialMediaMode: "camera",
initialSpatialMode: "3d",
});
const layerIds = useMemo(
() => new Set(viewerLayers.map((layer) => layer.layerId)),
[viewerLayers],
);
const hasProjectedLayers = viewerLayers.length > 0;
const hasDDRNet = layerIds.has("camera.ddrnet")
|| (!hasProjectedLayers && semantics && moduleIds.includes("ddrnet"));
const hasEoMT = layerIds.has("camera.eomt")
|| (!hasProjectedLayers && semantics && moduleIds.includes("eomt"));
const hasFallbackSemantics = semantics && !hasDDRNet && !hasEoMT;
const hasDetections = layerIds.has("camera.detections") || (!hasProjectedLayers && detections);
const hasTgs = layerIds.has("spatial.tgs") || (!hasProjectedLayers && costmap);
const legacyUnclassified = !hasProjectedLayers
&& moduleIds.length === 0
&& !semantics
&& !detections
&& !costmap;
const hasCameraPane = layerIds.has("camera.source")
|| (!hasProjectedLayers && (legacyUnclassified || semantics || detections));
const hasSourcePoints = layerIds.has("spatial.source-points")
|| (!hasProjectedLayers && (legacyUnclassified || costmap || moduleIds.includes("object-distance")));
const hasLocalSlam = layerIds.has("spatial.local-slam")
|| (!hasProjectedLayers && (legacyUnclassified || costmap || moduleIds.includes("object-distance")));
const hasSpatialPane = hasSourcePoints || hasLocalSlam || hasTgs;
const splitView = hasCameraPane && hasSpatialPane;
const [showCamera, setShowCamera] = useState(true);
const [showDDRNet, setShowDDRNet] = useState(hasDDRNet || hasFallbackSemantics);
const [showEoMT, setShowEoMT] = useState(hasEoMT);
const [showDetections, setShowDetections] = useState(hasDetections);
const [showSourcePoints, setShowSourcePoints] = useState(hasSourcePoints);
const [showLocalSlam, setShowLocalSlam] = useState(hasLocalSlam);
const [showTgs, setShowTgs] = useState(hasTgs);
const [followTarget, setFollowTarget] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [sceneDraft, setSceneDraft] = useState<SceneSettings>(() => loadLabSceneSettings(resultId));
const [blueprintSplitPrimarySize, setBlueprintSplitPrimarySize] = useState(
RERUN_UNIFIED_CAMERA_SHARE_PERCENT,
);
const [toasts, setToasts] = useState<ToastItem[]>([]);
const [viewResetGeneration, setViewResetGeneration] = useState<0 | 1>(hasTgs ? 1 : 0);
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
const [playbackController, setPlaybackController] = useState<RerunPlaybackController | null>(null);
const [viewerStatus, setViewerStatus] = useState<RerunViewportStatus>("idle");
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
const [launchError, setLaunchError] = useState<string | null>(null);
const sceneDraftRef = useRef(sceneDraft);
const previousSplitViewRef = useRef(splitView);
const trackedLaunchSha256Ref = useRef<string | null>(null);
useEffect(() => {
const launchSha256 = launch?.replay.sha256 ?? null;
if (trackedLaunchSha256Ref.current !== launchSha256 || (splitView && !previousSplitViewRef.current)) {
onSplitPrimarySizeChange(RERUN_UNIFIED_CAMERA_SHARE_PERCENT);
setBlueprintSplitPrimarySize(RERUN_UNIFIED_CAMERA_SHARE_PERCENT);
}
trackedLaunchSha256Ref.current = launchSha256;
previousSplitViewRef.current = splitView;
}, [launch?.replay.sha256, onSplitPrimarySizeChange, splitView]);
useEffect(() => {
if (!splitView) return;
const timer = window.setTimeout(() => setBlueprintSplitPrimarySize(splitPrimarySize), 75);
return () => window.clearTimeout(timer);
}, [splitPrimarySize, splitView]);
useEffect(() => {
try {
window.localStorage.setItem(
`${LAB_SCENE_SETTINGS_STORAGE_PREFIX}${resultId}`,
JSON.stringify({
pointSize: sceneDraft.pointSize,
accumulationSeconds: sceneDraft.accumulationSeconds,
colorMode: sceneDraft.colorMode,
palette: sceneDraft.palette,
showGrid: sceneDraft.showGrid,
showLabels: sceneDraft.showLabels,
showCameraFrustums: sceneDraft.showCameraFrustums,
}),
);
} catch {
// The visual remains usable when the browser refuses local persistence.
}
}, [resultId, sceneDraft]);
useEffect(() => {
const controller = new AbortController();
void fetchLabViewProfile(resultId, controller.signal).then((settings) => {
if (!controller.signal.aborted && settings) {
sceneDraftRef.current = settings;
setSceneDraft(settings);
}
}).catch(() => {
// The per-browser copy remains a usable fallback while the local service recovers.
});
return () => controller.abort();
}, [resultId]);
useEffect(() => {
const controller = new AbortController();
setLaunch(null);
setLaunchError(null);
setPlayback(null);
setPlaybackController(null);
setViewerStatus("idle");
void resolveObservationSessionReplay(sessionId, {
signal: controller.signal,
maximumWaitMs: 30 * 60 * 1000,
onUpdate: () => undefined,
}).then(async (value) => ({
base: value,
replay: await resolveReplay(resultId, value, { signal: controller.signal }),
})).then((value) => {
if (!controller.signal.aborted) setLaunch(value);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setLaunchError(caught instanceof Error ? caught.message : "Сохранённая запись недоступна.");
}
});
return () => controller.abort();
}, [resultId, sessionId, resolveReplay]);
const presentationReady = playbackController !== null
&& isRecordedPlaybackPresentationReady(viewerStatus, playback);
const presentationState = launchError || viewerStatus === "error"
? "error"
: presentationReady ? "ready" : "loading";
const sceneSettings = useMemo<SceneSettings>(() => ({
...sceneDraft,
projection: spatialMode === "plan" ? "2d" : "3d",
showPoints: spatialMode !== null && (showSourcePoints || showLocalSlam),
showTrajectory: spatialMode !== null && showLocalSlam,
accumulationSeconds: showLocalSlam ? sceneDraft.accumulationSeconds : 0,
}), [sceneDraft, showLocalSlam, showSourcePoints, spatialMode]);
const semanticVisible = showDDRNet || showEoMT;
const profile = launch ? recordedSessionRerunProfile({
sourceUrl: launch.replay.sourceUrl,
artifact: {
sourceUrl: launch.replay.sourceUrl,
viewerSourceUrl: launch.replay.viewerSourceUrl,
byteLength: launch.replay.byteLength,
sha256: launch.replay.sha256,
},
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
initialPlaybackStartSeconds: initialPlaybackStartSeconds
?? ((launch.base.mediaSources[0]?.timelineStartSeconds ?? launch.base.timelineStartSeconds)
+ (hasTgs ? 0.000001 : 0)),
view: hasCameraPane && !hasSpatialPane ? "perception" : "spatial",
viewResetGeneration,
followTrajectory: followTarget,
semanticLayer: showEoMT ? "city" : "vegetation",
unifiedPerception: splitView,
unifiedCameraShare: blueprintSplitPrimarySize / 100,
planView: spatialMode === "plan",
perceptionLayers: {
enabled: true,
cameraImage: showCamera,
detections2d: hasDetections && showDetections,
segmentation: semantics && semanticVisible,
cuboids3d: false,
costmap: hasTgs && showTgs,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: false,
}) : null;
const toggle = (caption: string, active: boolean, onClick: () => void) => (
<Button key={caption} size="dense" shape="pill" variant={active ? "primary" : "secondary"}
aria-pressed={active} onClick={onClick}>
{caption}
</Button>
);
const openSlamSettings = useCallback(() => {
if (expanded) onExpandedChange(false);
setSettingsOpen(true);
}, [expanded, onExpandedChange]);
const patchSceneDraft = useCallback((patch: Partial<SceneSettings>) => {
const next = { ...sceneDraftRef.current, ...patch };
sceneDraftRef.current = next;
setSceneDraft(next);
}, []);
const closeSlamSettings = useCallback(() => {
const next = normalizeLabSceneSettings(sceneDraftRef.current);
sceneDraftRef.current = next;
setSceneDraft(next);
setSettingsOpen(false);
void saveLabViewProfile(resultId, next).then((settings) => {
sceneDraftRef.current = settings;
setSceneDraft(settings);
setToasts((current) => current.filter(({ id }) => id !== LAB_VIEW_PROFILE_ERROR_TOAST));
}).catch(() => {
setToasts((current) => [
...current.filter(({ id }) => id !== LAB_VIEW_PROFILE_ERROR_TOAST),
{
id: LAB_VIEW_PROFILE_ERROR_TOAST,
tone: "error",
title: "Настройки LAB не сохранились",
description: "Изменения остались в текущем просмотре. Откройте настройки и повторите.",
},
]);
});
}, [resultId]);
const mediaLayerControls = (
<div className="m4-replay-threat-visual__pane-layer-controls" role="group" aria-label="Слои камеры">
{toggle("КАМЕРА", showCamera, () => setShowCamera((value) => !value))}
{hasEoMT ? toggle("EOMT", showEoMT, () => setShowEoMT((value) => !value)) : null}
{hasDDRNet || hasFallbackSemantics
? toggle("DDRNET", showDDRNet, () => setShowDDRNet((value) => !value)) : null}
{hasDetections
? toggle("РАМКИ", showDetections, () => setShowDetections((value) => !value)) : null}
</div>
);
const spatialLayerControls = (
<div className="m4-replay-threat-visual__pane-layer-controls" role="group" aria-label="Слои облака точек">
{hasSourcePoints
? toggle("ИСХ. ТОЧКИ", showSourcePoints, () => setShowSourcePoints((value) => !value)) : null}
{hasLocalSlam ? (
<div className="m4-replay-threat-visual__layer-with-settings">
{toggle("ЛОК. SLAM", showLocalSlam, () => setShowLocalSlam((value) => !value))}
<IconButton label="Настройки облака SLAM" onClick={openSlamSettings}>
<Icon name="settings" size={14} />
</IconButton>
</div>
) : null}
{hasTgs ? toggle("TGS", showTgs, () => setShowTgs((value) => !value)) : null}
</div>
);
const followTargetControl = toggle(
"СЛЕДОВАТЬ",
followTarget,
() => setFollowTarget((value) => !value),
);
const resetSpatialView = (
<IconButton label="Сбросить положение видов"
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}>
<Icon name="refresh" size={16} />
</IconButton>
);
const transport = presentationReady && playback && playbackController ? (
<ObservationTimeline
className="m4-replay-threat-visual__timeline canonical-vegetation-rerun-replay__timeline"
active
sourceCount={3}
mode="recorded"
seekable
synchronization="shared-clock"
rangeNs={playback.rangeNs}
currentNs={playback.currentNs}
playing={playback.playing}
onSeek={playbackController.seek}
onPlayingChange={playbackController.setPlaying}
showJumpToEnd={false}
/>
) : undefined;
return (
<div className="canonical-vegetation-rerun-replay" data-presentation-state={presentationState}
aria-busy={presentationState === "loading"}>
<CanonicalRecordedLabReplay
label={label}
mediaMode={hasCameraPane ? mediaMode ?? "camera" : "none"}
mediaModes={[{ value: "camera", label: "КАМЕРА" }]}
spatialMode={hasSpatialPane ? spatialMode ?? "3d" : "none"}
spatialModes={[{ value: "3d", label: "3D" }, { value: "plan", label: "ПЛАН" }]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitView ? "vertical" : splitOrientation}
mediaAriaLabel="Камера и рассчитанные слои"
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialTrailingControl={followTargetControl}
mediaModeControlsVisible={false}
paneToolbarsAlwaysVisible
mediaMultiLayer
actions={resetSpatialView}
unifiedContent={profile ? (
<div className="canonical-vegetation-rerun-replay__viewport-lock"
data-split-view={splitView ? "true" : undefined}>
<RerunViewport profile={profile} sceneSettings={sceneSettings}
onStatusChange={setViewerStatus} onPlaybackChange={setPlayback}
onPlaybackControllerChange={setPlaybackController} />
</div>
) : launchError ? (
<div className="l3-visual-audit__state" role="alert">{launchError}</div>
) : <div aria-hidden="true" />}
emptyMessage="Слои результата недоступны."
deckOverlays={presentationState === "loading" ? (
<div className="canonical-vegetation-rerun-replay__loading">
<ActivityIndicator label="Загружаем синхронизированную запись" />
</div>
) : undefined}
transport={transport}
onMediaModeChange={onMediaModeChange}
onSpatialModeChange={onSpatialModeChange}
onExpandedChange={onExpandedChange}
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
/>
<Window open={settingsOpen} title="Отображение" subtitle="Параметры облака SLAM"
placement="end" size="sm" onClose={closeSlamSettings}>
<Inspector defaultOpen={["points"]} sections={[
{
id: "points",
label: "Облако точек",
description: "Размер и способ окрашивания",
content: <>
<RangeControl label="Размер точки" min={0.1} max={10} step={0.1}
exactValueBounds={{ min: 0.1 }}
value={sceneDraft.pointSize} formatValue={(value) => `${value.toFixed(1)} пкс`}
onChange={(pointSize) => patchSceneDraft({ pointSize })} />
<InspectorSelectField label="Атрибут цвета" value={sceneDraft.colorMode}
options={[...COLOR_OPTIONS]}
onChange={(colorMode) => patchSceneDraft({ colorMode })} />
<InspectorSelectField label="Палитра" value={sceneDraft.palette}
options={[...PALETTE_OPTIONS]}
onChange={(palette) => patchSceneDraft({ palette })} />
</>,
},
{
id: "history",
label: "Накопление и время",
description: "История облака и траектория",
content: <RangeControl label="Накопление" min={0} max={500} step={1}
exactValueBounds={{ min: 0 }}
value={sceneDraft.accumulationSeconds} formatValue={(value) => `${value} с`}
onChange={(accumulationSeconds) => patchSceneDraft({ accumulationSeconds })} />,
},
{
id: "environment",
label: "Окружение сцены",
description: "Сетка, подписи и камеры",
content: <>
<ControlRow label="Сетка"><Checker checked={sceneDraft.showGrid} label="Показывать"
onChange={(showGrid) => patchSceneDraft({ showGrid })} /></ControlRow>
<ControlRow label="Подписи"><Checker checked={sceneDraft.showLabels} label="Показывать"
onChange={(showLabels) => patchSceneDraft({ showLabels })} /></ControlRow>
<ControlRow label="Камеры"><Checker checked={sceneDraft.showCameraFrustums} label="Показывать"
onChange={(showCameraFrustums) => patchSceneDraft({ showCameraFrustums })} /></ControlRow>
</>,
},
]} />
</Window>
<ToastStack items={toasts}
onDismiss={(id) => setToasts((current) => current.filter((item) => item.id !== id))} />
</div>
);
}
@@ -0,0 +1,12 @@
import { CanonicalResultRerunReplay } from "./CanonicalResultRerunReplay";
import { resolveCanonicalLabReplay } from "../../core/laboratory/canonicalLabReplay";
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
export function CanonicalVegetationRerunReplay({ resultId, review }: {
resultId: string;
review: VegetationFullRouteReview;
}) {
return <CanonicalResultRerunReplay resultId={resultId} sessionId={review.sessionId}
initialPlaybackStartSeconds={review.timelineStartSeconds} semantics
resolveReplay={resolveCanonicalLabReplay} label="RAVNOVES004TREE · канонический повтор Rerun" />;
}
@@ -0,0 +1,51 @@
import { GlassSurface } from "@nodedc/ui-react";
import { CanonicalResultRerunReplay } from "./CanonicalResultRerunReplay";
import { resolveCanonicalLabReplay } from "../../core/laboratory/canonicalLabReplay";
import type { ObservatoryPortableResultReview } from "../../core/observatory/recordedRun";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
function resolvePortableTgsReplay(resultId: string, launch: ObservationSessionReplayLaunch,
options: { signal: AbortSignal }) {
return resolveCanonicalLabReplay(resultId, launch, { ...options, sourceKind: "portable-tgs" });
}
function resolvePortableSemanticReplay(resultId: string, launch: ObservationSessionReplayLaunch,
options: { signal: AbortSignal }) {
return resolveCanonicalLabReplay(resultId, launch, { ...options, sourceKind: "portable-semantic" });
}
function resolvePortableObjectReplay(resultId: string, launch: ObservationSessionReplayLaunch,
options: { signal: AbortSignal }) {
return resolveCanonicalLabReplay(resultId, launch, { ...options, sourceKind: "portable-objects" });
}
export function PortableResultReplay({
review,
label,
}: {
review: ObservatoryPortableResultReview;
label?: string;
}) {
const schema = review.resultDocument.schema_version;
const tgs = schema === "missioncore.recorded-tgs-costmap-review/v2";
const module = review.resultDocument.module;
const moduleId = module && typeof module === "object" && "module_id" in module
? module.module_id : undefined;
const semantics = schema === "missioncore.recorded-eomt-ddrnet-review/v2"
|| (schema === "missioncore.recorded-ai-layer-review/v1"
&& (moduleId === "ddrnet" || moduleId === "eomt"));
const objects = schema === "missioncore.recorded-ai-layer-review/v1"
&& (moduleId === "rf-detr" || moduleId === "object-distance");
if (!tgs && !semantics && !objects) return (
<GlassSurface className="observatory-replay-state" padding="lg">
<p role="alert">Сохранённый результат этого профиля пока не поддерживает визуальный разбор.</p>
</GlassSurface>
);
return <CanonicalResultRerunReplay key={review.resultId} resultId={review.resultId}
sessionId={review.sourceSessionId} portable costmap={tgs} semantics={semantics}
detections={objects}
moduleIds={typeof moduleId === "string" ? [moduleId] : []}
label={label}
resolveReplay={tgs ? resolvePortableTgsReplay
: objects ? resolvePortableObjectReplay : resolvePortableSemanticReplay} />;
}
@@ -0,0 +1,261 @@
import { useEffect, useMemo, useState } from "react";
import {
ActivityIndicator,
Button,
FieldFrame,
Select,
StatusBadge,
Window,
WindowFooterActions,
} from "@nodedc/ui-react";
import {
aiModuleIdForSetup,
fetchAIModuleCatalog,
fetchAICompositionRuns,
fetchAICompositionJobs,
saveAIComposition,
type AICompositionReceipt,
type AICompositionRun,
type AIGroupId,
type AIModuleCatalog,
} from "../../core/observatory/aiComposition";
import type { ObservatoryRecordedJob } from "../../core/observatory/recordedJobs";
const groups: readonly { id: AIGroupId; label: string; description: string }[] = [
{ id: "segmentation", label: "Сегментация", description: "Пиксельные маски сцены" },
{ id: "detection", label: "Объекты", description: "Рамки и классы объектов" },
{ id: "geometry", label: "Облако точек / TGS", description: "Опорная поверхность и карта проходимости" },
{ id: "range", label: "Дистанция", description: "Расстояния до объектов" },
{ id: "motion", label: "Движение", description: "Треки и динамика объектов" },
{ id: "policy", label: "Политика", description: "Теневые правила интерпретации" },
];
export function AIConfigurationWindow({
open, sourceSessionId, sourceLabel, onClose, onCalculated,
}: {
readonly open: boolean;
readonly sourceSessionId: string;
readonly sourceLabel: string;
readonly onClose: () => void;
readonly onCalculated: (receipt: AICompositionReceipt) => void;
}) {
const [catalog, setCatalog] = useState<AIModuleCatalog | null>(null);
const [existingJobs, setExistingJobs] = useState<readonly ObservatoryRecordedJob[]>([]);
const [existingRuns, setExistingRuns] = useState<readonly AICompositionRun[]>([]);
const [selected, setSelected] = useState<Partial<Record<AIGroupId, string>>>({});
const [state, setState] = useState<"idle" | "loading" | "ready" | "saving" | "error">("idle");
const [message, setMessage] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
const request = new AbortController();
setState("loading");
setMessage(null);
void Promise.all([
fetchAIModuleCatalog(request.signal),
fetchAICompositionJobs(sourceSessionId, request.signal),
fetchAICompositionRuns(sourceSessionId, request.signal),
]).then(([value, jobs, runs]) => {
if (request.signal.aborted) return;
setCatalog(value);
setExistingJobs(jobs);
setExistingRuns(runs);
setSelected({});
setState("ready");
}).catch((error: unknown) => {
if (request.signal.aborted) return;
setMessage(error instanceof Error ? error.message : "Каталог AI-модулей недоступен.");
setState("error");
});
return () => request.abort();
}, [open, sourceSessionId]);
const existingByModule = useMemo(() => {
const result = new Map<string, ObservatoryRecordedJob>();
for (const job of existingJobs) {
if (job.state === "failed" || job.publication.state === "failed") continue;
const moduleId = aiModuleIdForSetup(job.setupId);
if (moduleId && !result.has(moduleId)) result.set(moduleId, job);
}
return result;
}, [existingJobs]);
const selections = useMemo(() => groups.flatMap(({ id }) => {
const moduleId = selected[id];
const module = catalog?.groups.find((item) => item.group === id)?.modules.find(
(item) => item.moduleId === moduleId,
);
return module ? [{ group: id, module }] : [];
}), [catalog, selected]);
const selectedModuleIds = useMemo(
() => selections.map(({ module }) => module.moduleId).sort(),
[selections],
);
const matchingRun = useMemo(() => existingRuns.find((run) => (
run.state !== "failed"
&& [...run.moduleIds].sort().join("\0") === selectedModuleIds.join("\0")
)), [existingRuns, selectedModuleIds]);
const configurationAlreadyExists = selections.length > 0 && matchingRun !== undefined;
const configurationIsRunning = matchingRun?.state === "running";
function selectModule(group: AIGroupId, moduleId: string): void {
if (!catalog) return;
setSelected((current) => {
const next = { ...current, [group]: moduleId === "none" ? undefined : moduleId };
const modules = catalog.groups.flatMap((item) => item.modules);
const selectedIds = new Set(Object.values(next).filter((value): value is string => Boolean(value)));
if (moduleId !== "none") {
const module = modules.find((item) => item.moduleId === moduleId);
for (const requirement of module?.requires ?? []) {
if (requirement.startsWith("source.")) continue;
if (modules.some((item) => selectedIds.has(item.moduleId) && item.provides.includes(requirement))) {
continue;
}
const providers = modules.filter((item) => item.provides.includes(requirement));
if (providers.length === 1) {
const providerGroup = catalog.groups.find((item) => item.modules.some(
(candidate) => candidate.moduleId === providers[0]?.moduleId,
))?.group;
if (providerGroup) {
next[providerGroup] = providers[0]?.moduleId;
selectedIds.add(providers[0]?.moduleId ?? "");
}
}
}
} else {
let changed = true;
while (changed) {
changed = false;
const capabilities = new Set(modules.filter((item) => Object.values(next).includes(item.moduleId))
.flatMap((item) => item.provides));
for (const item of modules.filter((candidate) => Object.values(next).includes(candidate.moduleId))) {
if (item.requires.some((requirement) => !requirement.startsWith("source.") && !capabilities.has(requirement))) {
const dependentGroup = catalog.groups.find((candidate) => candidate.modules.some(
(module) => module.moduleId === item.moduleId,
))?.group;
if (dependentGroup) {
next[dependentGroup] = undefined;
changed = true;
}
}
}
}
}
return next;
});
setMessage(null);
if (state === "error") setState("ready");
}
async function calculate(): Promise<void> {
if (selections.length === 0 || state === "saving") return;
if (configurationAlreadyExists) {
setMessage("Эта конфигурация уже рассчитана. Выберите другую конфигурацию.");
return;
}
setState("saving");
setMessage(null);
try {
const receipt = await saveAIComposition(sourceSessionId, selections);
onCalculated(receipt);
onClose();
} catch (error) {
setMessage(error instanceof Error ? error.message : "Не удалось запустить расчёт.");
setState("error");
}
}
return <Window
open={open}
className="observatory-ai-config-window"
title="Сконфигурировать AI-слой"
subtitle={sourceLabel}
size="lg"
closeOnBackdrop={state !== "saving"}
closeOnEscape={state !== "saving"}
onClose={onClose}
footer={<WindowFooterActions>
{configurationAlreadyExists ? (
<span className="observatory-ai-config__duplicate" role="status">
{configurationIsRunning
? "Текущая конфигурация уже рассчитывается."
: "Текущая конфигурация уже рассчитана."}
</span>
) : null}
<Button
variant="primary"
className={state === "saving" ? "observatory-ai-config__calculate--saving" : undefined}
disabled={state !== "ready" || selections.length === 0 || configurationAlreadyExists}
aria-busy={state === "saving"}
onClick={() => void calculate()}
>
{state === "saving" ? "Отправляем на Worker…" : "Рассчитать"}
</Button>
</WindowFooterActions>}
>
<div className="observatory-ai-config" aria-busy={state === "loading" || state === "saving"}>
<p className="observatory-ai-config__lead">
Выберите модуль в каждом нужном слое. Зависимые блоки подключаются явно в этой конфигурации.
</p>
{state === "loading" ? <ActivityIndicator label="Загружаем AI-модули" /> : null}
{catalog ? groups.map(({ id, label, description }) => {
const modules = catalog.groups.find((item) => item.group === id)?.modules ?? [];
const selectedModule = modules.find((item) => item.moduleId === selected[id]);
const selectedExisting = selectedModule
? existingByModule.get(selectedModule.moduleId)
: undefined;
const allCalculated = modules.length > 0 && modules.every(
(module) => existingByModule.has(module.moduleId),
);
return <section className="observatory-ai-module-group" key={id}>
<header>
<div>
<strong>{label}</strong>
<span>{description}</span>
</div>
<StatusBadge tone={selectedModule ? "accent" : "neutral"}>
{selectedExisting
? selectedExisting.state === "succeeded" ? "Уже рассчитано" : "В расчёте"
: selectedModule?.label ?? (allCalculated
? "Результат доступен"
: modules.length ? "Не используется" : "Не установлен")}
</StatusBadge>
</header>
<div className="observatory-ai-module-group__body">
<FieldFrame
label={`Модуль: ${label}`}
description={modules.length
? allCalculated
? "Готовый результат можно включить в новую конфигурацию без повторного расчёта."
: id === "segmentation"
? "DDRNet и EoMT — независимые альтернативы; выберите одну модель."
: id === "range"
? "Дистанция использует рамки детектора и синхронное облако точек."
: id === "geometry"
? "TGS обрабатывает LiDAR независимо от сегментации и детектора."
: "Связи с другими блоками определяются входами выбранного модуля."
: "Для этого слоя на Worker 006 пока нет установленного модуля."}
>
<Select
label={`Выбрать модуль: ${label}`}
value={selected[id] ?? "none"}
options={[{ value: "none", label: "Не использовать" }, ...modules.map((module) => ({
value: module.moduleId,
label: module.label,
description: existingByModule.has(module.moduleId)
? "Готовый результат будет использован без повторного расчёта"
: module.dockerName,
}))]}
disabled={modules.length === 0 || state === "saving"}
onChange={(value) => selectModule(id, value)}
/>
</FieldFrame>
</div>
</section>;
}) : null}
{message ? <p className="observatory-ai-config__error" role="alert">{message}</p> : null}
</div>
</Window>;
}
@@ -0,0 +1,10 @@
import { WebViewer } from "@rerun-io/web-viewer";
import type { RerunFrameWindow } from "./recordedRerunProtocol";
import { createRecordedRerunOwner } from "./recordedRerunOwner";
// This entry is loaded only in the owned iframe. Upstream code is unmodified;
// destroying the frame also ends its timers, WASM, video and WebGPU lifetime.
const mount = document.getElementById("viewer");
if (!mount) throw new Error("Recorded viewer mount is missing");
(window as RerunFrameWindow).missionCoreRerun = createRecordedRerunOwner(() => new WebViewer(), mount);
@@ -0,0 +1,77 @@
import { createRecordedRerunFacade, type RecordedRerunViewer } from "./recordedRerunFacade";
import type { RerunFrameWindow } from "./recordedRerunProtocol";
import { relayIsolatedRerunInput } from "./isolatedRerunInput";
/** Own the complete recorded-viewer realm, not just its canvas or SDK handle. */
export function createIsolatedRerunHost(host: HTMLElement) {
let frame: HTMLIFrameElement | null = host.ownerDocument.createElement("iframe");
frame.title = "Синхронизированная запись";
frame.className = "rerun-viewport__runtime";
frame.src = "/rerun-runtime.html";
let disposed = false;
let releaseFacade: (() => void) | null = null;
let releaseInput: (() => void) | null = null;
let frameWindow: RerunFrameWindow | null = null;
let rejectReady: ((reason: Error) => void) | null = null;
let timer: ReturnType<typeof setTimeout> | undefined;
const dispose = () => {
if (disposed) return;
disposed = true;
clearTimeout(timer);
frame!.onload = null;
frame!.onerror = null;
releaseInput?.();
releaseInput = null;
try {
releaseFacade?.();
} catch {
// Realm teardown is required even if a partial native start failed.
} finally {
releaseFacade = null;
frameWindow = null;
frame!.remove();
frame = null;
rejectReady?.(new Error("Recorded viewer was disposed"));
rejectReady = null;
}
};
const ready = new Promise<{ viewer: RecordedRerunViewer; mount: HTMLElement }>((resolve, reject) => {
rejectReady = reject;
frame!.onload = () => {
if (disposed) return;
try {
frameWindow = frame!.contentWindow as RerunFrameWindow | null;
const api = frameWindow?.missionCoreRerun;
if (!api || api.version !== 2) throw new Error("Recorded viewer host is unavailable");
const owner = createRecordedRerunFacade((request, bytes) => {
const current = frameWindow?.missionCoreRerun;
if (!current) throw new Error("Recorded viewer was disposed");
return current.invoke(request, bytes);
});
releaseFacade = owner.dispose;
api.connect(owner.notify);
releaseInput = relayIsolatedRerunInput(host, frameWindow, frame);
clearTimeout(timer);
rejectReady = null;
frame!.onload = null;
frame!.onerror = null;
resolve({ viewer: owner.facade, mount: host });
} catch (error) {
reject(new Error(String(error)));
dispose();
}
};
frame!.onerror = () => {
reject(new Error("Recorded viewer host failed to load"));
dispose();
};
timer = setTimeout(() => {
reject(new Error("Recorded viewer host timed out"));
dispose();
}, 30_000);
});
host.append(frame);
return { ready, dispose };
}
@@ -0,0 +1,17 @@
/** Relay Escape from the isolated viewer realm to the owning application. */
export function relayIsolatedRerunInput(
host: HTMLElement,
frameWindow: Window | null,
_frame: HTMLIFrameElement | null,
) {
const parent = host.ownerDocument.defaultView;
const escape = (event: KeyboardEvent) => {
if (event.key !== "Escape" || event.defaultPrevented || !parent) return;
host.dispatchEvent(new parent.KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
};
frameWindow?.addEventListener("keydown", escape);
return () => {
frameWindow?.removeEventListener("keydown", escape);
frameWindow = null;
};
}
@@ -0,0 +1,244 @@
export type RecordedRerunCameraEye = {
readonly position: readonly [number, number, number];
readonly lookTarget: readonly [number, number, number];
readonly eyeUp: readonly [number, number, number];
};
type Vector3 = [number, number, number];
type DragMode = "rotate" | "pan" | "roll";
const ROTATION_RADIANS_PER_POINT = 0.004;
const RERUN_WEB_LINE_SCROLL_POINTS = 8;
const RERUN_ORBITAL_SCROLL_DIVISOR = 200;
const RERUN_WEB_PINCH_SCROLL_DIVISOR = 100;
const RERUN_MIN_ORBIT_DISTANCE = 0.02;
const DOM_WHEEL_DELTA_LINE = 1;
const DOM_WHEEL_DELTA_PAGE = 2;
export const RECORDED_RERUN_ORBITAL_EYE: RecordedRerunCameraEye = {
position: [16, -16, 18],
lookTarget: [0, 0, 0],
eyeUp: [0, 0, 1],
};
export const RECORDED_RERUN_PLAN_EYE: RecordedRerunCameraEye = {
position: [0, 0, 30],
lookTarget: [0, 0, 0],
eyeUp: [0, 1, 0],
};
const vector = (value: readonly [number, number, number]): Vector3 => [...value];
const add = (a: Vector3, b: Vector3): Vector3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
const subtract = (a: Vector3, b: Vector3): Vector3 => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
const scale = (value: Vector3, factor: number): Vector3 => [value[0] * factor, value[1] * factor, value[2] * factor];
const dot = (a: Vector3, b: Vector3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const cross = (a: Vector3, b: Vector3): Vector3 => [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
const length = (value: Vector3) => Math.hypot(...value);
const normalize = (value: Vector3, fallback: Vector3): Vector3 => {
const magnitude = length(value);
return magnitude > Number.EPSILON ? scale(value, 1 / magnitude) : fallback;
};
const clamp = (value: number, minimum: number, maximum: number) => Math.max(minimum, Math.min(maximum, value));
const rotateAroundAxis = (value: Vector3, rawAxis: Vector3, angle: number): Vector3 => {
const axis = normalize(rawAxis, [0, 0, 1]);
const cosine = Math.cos(angle);
const sine = Math.sin(angle);
return add(
add(scale(value, cosine), scale(cross(axis, value), sine)),
scale(axis, dot(axis, value) * (1 - cosine)),
);
};
function cloneEye(value: RecordedRerunCameraEye): {
position: Vector3;
lookTarget: Vector3;
eyeUp: Vector3;
} {
return {
position: vector(value.position),
lookTarget: vector(value.lookTarget),
eyeUp: vector(value.eyeUp),
};
}
/**
* Mirrors Rerun 0.36.3's orbital eye math while the native viewer remains the
* renderer. Rerun writes operator navigation into its active blueprint clone;
* the WebViewer API cannot read that clone. Keeping the same three eye vectors
* here lets a later layer blueprint activate with the exact operator pose.
*/
export function createRecordedRerunCameraJournal(
canvas: HTMLCanvasElement,
scope: Window & typeof globalThis,
) {
let eye = cloneEye(RECORDED_RERUN_ORBITAL_EYE);
let spatialViewportStart = 0;
let pointerId: number | null = null;
let dragMode: DragMode | null = null;
let previousPointer: readonly [number, number] | null = null;
let latestPointer: readonly [number, number] | null = null;
let maxOrbitalRadius = 1.0e17;
const isSpatialPoint = (event: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
return rect.width > 0 && (event.clientX - rect.left) / rect.width >= spatialViewportStart;
};
const forward = () => normalize(subtract(eye.lookTarget, eye.position), [0, 1, 0]);
const usableUp = () => {
const fwd = forward();
const fallbackRight: Vector3 = Math.abs(dot(fwd, [0, 0, 1])) > 0.9999
? [1, 0, 0]
: cross(fwd, [0, 0, 1]);
const fallback = normalize(cross(fwd, fallbackRight), [0, 0, 1]);
const candidate = normalize(eye.eyeUp, fallback);
return Math.abs(dot(candidate, fwd)) > 0.9999 ? fallback : candidate;
};
const orbitRadius = () => length(subtract(eye.position, eye.lookTarget));
const rotate = (deltaX: number, deltaY: number) => {
const radius = orbitRadius();
const up = usableUp();
let fwd = forward();
const oldPitch = Math.asin(clamp(dot(fwd, up), -1, 1));
fwd = normalize(
rotateAroundAxis(fwd, up, -ROTATION_RADIANS_PER_POINT * deltaX),
fwd,
);
const right = normalize(cross(fwd, up), [1, 0, 0]);
const maxPitch = 0.99 * Math.PI / 2;
const nextPitch = clamp(
oldPitch - ROTATION_RADIANS_PER_POINT * deltaY,
-maxPitch,
maxPitch,
);
fwd = normalize(rotateAroundAxis(fwd, right, nextPitch - oldPitch), fwd);
eye.position = subtract(eye.lookTarget, scale(fwd, radius));
};
const pan = (deltaX: number, deltaY: number) => {
const fwd = forward();
const right = normalize(cross(fwd, usableUp()), [1, 0, 0]);
const screenUp = normalize(cross(right, fwd), [0, 0, 1]);
const speed = 0.001 * orbitRadius();
const translation = add(scale(right, -deltaX * speed), scale(screenUp, deltaY * speed));
eye.position = add(eye.position, translation);
eye.lookTarget = add(eye.lookTarget, translation);
};
const roll = (event: PointerEvent, deltaX: number, deltaY: number) => {
const rect = canvas.getBoundingClientRect();
const left = rect.left + rect.width * spatialViewportStart;
const centerX = left + (rect.right - left) / 2;
const centerY = rect.top + rect.height / 2;
const relativeX = event.clientX - centerX;
const relativeY = event.clientY - centerY;
const divisor = relativeX * relativeX + relativeY * relativeY;
if (divisor <= Number.EPSILON) return;
const angle = (-deltaY * relativeX + deltaX * relativeY) / divisor;
eye.eyeUp = normalize(rotateAroundAxis(eye.eyeUp, scale(forward(), -1), angle), eye.eyeUp);
};
const pointerDown = (event: PointerEvent) => {
latestPointer = [event.clientX, event.clientY];
if (!isSpatialPoint(event)) return;
pointerId = event.pointerId;
previousPointer = [event.clientX, event.clientY];
dragMode = event.button === 2 ? "pan" : event.button === 1 || event.altKey ? "roll" : "rotate";
};
const pointerMove = (event: PointerEvent) => {
latestPointer = [event.clientX, event.clientY];
if (event.pointerId !== pointerId || !previousPointer || !dragMode) return;
const deltaX = event.clientX - previousPointer[0];
const deltaY = event.clientY - previousPointer[1];
previousPointer = [event.clientX, event.clientY];
if (dragMode === "rotate") rotate(deltaX, deltaY);
else if (dragMode === "pan") pan(deltaX, deltaY);
else roll(event, deltaX, deltaY);
};
const pointerEnd = (event: PointerEvent) => {
if (event.pointerId !== pointerId) return;
pointerMove(event);
pointerId = null;
previousPointer = null;
dragMode = null;
};
const wheel = (event: WheelEvent) => {
const rect = canvas.getBoundingClientRect();
const wheelX = event.clientX >= rect.left && event.clientX <= rect.right
? event.clientX
: latestPointer?.[0];
if (wheelX === undefined || rect.width <= 0) return;
if ((wheelX - rect.left) / rect.width < spatialViewportStart) return;
const unitMultiplier = event.deltaMode === DOM_WHEEL_DELTA_LINE
? RERUN_WEB_LINE_SCROLL_POINTS
: event.deltaMode === DOM_WHEEL_DELTA_PAGE
? rect.height
: 1;
const scrollPoints = (event.deltaX + event.deltaY) * unitMultiplier;
// eframe negates the DOM delta before Rerun applies
// radius / exp(smooth_scroll_delta / 200). Over a smoothed gesture the
// exponent products collapse to the raw accumulated DOM delta below.
const divisor = event.ctrlKey
? RERUN_WEB_PINCH_SCROLL_DIVISOR
: RERUN_ORBITAL_SCROLL_DIVISOR;
const radiusFactor = Math.exp(scrollPoints / divisor);
const offset = subtract(eye.position, eye.lookTarget);
const radius = length(offset);
// Keep the same orbital bounds as Rerun 0.36.3. An eye already outside
// the scene-derived cap is allowed to stay there and is never snapped in.
const nextRadius = clamp(
radius * radiusFactor,
RERUN_MIN_ORBIT_DISTANCE,
Math.max(radius, maxOrbitalRadius),
);
eye.position = add(
eye.lookTarget,
scale(offset, nextRadius / Math.max(radius, Number.EPSILON)),
);
};
canvas.addEventListener("pointerdown", pointerDown, true);
scope.addEventListener("pointermove", pointerMove, true);
scope.addEventListener("pointerup", pointerEnd, true);
scope.addEventListener("pointercancel", pointerEnd, true);
scope.addEventListener("wheel", wheel, { capture: true, passive: true });
return {
current(): RecordedRerunCameraEye {
return {
position: [...eye.position],
lookTarget: [...eye.lookTarget],
eyeUp: [...eye.eyeUp],
};
},
configure(nextEye: RecordedRerunCameraEye, nextSpatialViewportStart: number) {
eye = cloneEye(nextEye);
spatialViewportStart = clamp(nextSpatialViewportStart, 0, 0.9);
pointerId = null;
dragMode = null;
previousPointer = null;
},
setSpatialViewportStart(nextSpatialViewportStart: number) {
spatialViewportStart = clamp(nextSpatialViewportStart, 0, 0.9);
},
setMaxOrbitalRadius(nextMaxOrbitalRadius: number) {
if (
Number.isFinite(nextMaxOrbitalRadius) &&
nextMaxOrbitalRadius >= RERUN_MIN_ORBIT_DISTANCE
) {
maxOrbitalRadius = nextMaxOrbitalRadius;
}
},
dispose() {
canvas.removeEventListener("pointerdown", pointerDown, true);
scope.removeEventListener("pointermove", pointerMove, true);
scope.removeEventListener("pointerup", pointerEnd, true);
scope.removeEventListener("pointercancel", pointerEnd, true);
scope.removeEventListener("wheel", wheel, true);
},
};
}
@@ -0,0 +1,105 @@
import type { WebViewer } from "@rerun-io/web-viewer";
import type { RerunInvoke } from "./recordedRerunProtocol";
import type { RecordedRerunCameraEye } from "./recordedRerunCameraJournal";
type NativeChannel = ReturnType<WebViewer["open_channel"]>;
type Channel = Pick<NativeChannel, "ready" | "send_rrd" | "close">;
export type RecordedRerunViewer = Pick<WebViewer,
"start" | "stop" | "ready" | "on" | "open" | "close" | "override_panel_state" |
"get_active_recording_id" | "get_active_timeline" | "get_current_time" |
"get_playing" | "get_time_range" | "set_active_timeline" |
"set_current_time" | "set_playing"
> & {
open_channel: (name?: string) => Channel;
configure_camera_journal: (eye: RecordedRerunCameraEye, spatialViewportStart: number) => void;
get_camera_eye: () => RecordedRerunCameraEye;
set_camera_viewport_start: (spatialViewportStart: number) => void;
set_camera_max_orbital_radius: (maxOrbitalRadius: number) => void;
};
/** Parent-owned values only. No SDK Promise or foreign prototype escapes. */
export function createRecordedRerunFacade(invoke: RerunInvoke | null) {
let nextId = 0;
const callbacks = new Map<number, (...args: unknown[]) => void>();
const starts = new Map<number, { resolve: () => void; reject: (error: Error) => void }>();
const command = (method: string, args: unknown[] = [], id?: number, bytes?: Uint8Array) => {
if (!invoke) throw new Error("Recorded viewer was disposed");
const response = JSON.parse(invoke(JSON.stringify({ method, args, id }), bytes));
if (response.error) throw new Error(response.error);
return response.result;
};
const dispose = () => {
callbacks.clear();
for (const pending of starts.values()) pending.reject(new Error("Recorded viewer was disposed"));
starts.clear();
try { if (invoke) command("stop"); } finally { invoke = null; }
};
const notify = (message: string) => {
if (!invoke) return;
const event = JSON.parse(message);
if (event.type === "event") callbacks.get(event.id)?.(...event.values);
else {
const pending = starts.get(event.id);
starts.delete(event.id);
if (event.type === "started") pending?.resolve();
else pending?.reject(new Error(event.message));
}
};
const facade: RecordedRerunViewer = {
get ready() { return invoke ? command("ready") : false; },
start(source, _parent, options) {
return new Promise<void>((resolve, reject) => {
const id = ++nextId;
starts.set(id, { resolve, reject });
try { command("start", [source, options], id); }
catch (error) { starts.delete(id); reject(error); }
});
},
stop: dispose,
on: ((event: string, callback: (...args: unknown[]) => void) => {
const id = ++nextId;
callbacks.set(id, callback);
try { command("on", [event], id); }
catch (error) { callbacks.delete(id); throw error; }
return () => {
if (callbacks.delete(id) && invoke) command("off", [], id);
};
}) as WebViewer["on"],
open_channel(name) {
const id = ++nextId;
command("channel-open", name === undefined ? [] : [name], id);
let closed = false;
return {
get ready() { return !closed && invoke ? command("channel-ready", [], id) : false; },
send_rrd(bytes) { if (!closed && invoke) command("channel-send", [], id, bytes); },
close() {
if (closed) return;
closed = true;
if (invoke) command("channel-close", [], id);
},
};
},
open: (...args) => command("open", args),
close: (...args) => { if (invoke) command("close", args); },
override_panel_state: (...args) => command("override_panel_state", args),
get_active_recording_id: () => command("get_active_recording_id"),
get_active_timeline: (...args) => command("get_active_timeline", args),
get_current_time: (...args) => command("get_current_time", args),
get_playing: (...args) => command("get_playing", args),
get_time_range: (...args) => command("get_time_range", args),
set_active_timeline: (...args) => command("set_active_timeline", args),
set_current_time: (...args) => command("set_current_time", args),
set_playing: (...args) => command("set_playing", args),
configure_camera_journal: (eye, spatialViewportStart) => command(
"configure-camera-journal", [eye, spatialViewportStart],
),
get_camera_eye: () => command("get-camera-eye"),
set_camera_viewport_start: (spatialViewportStart) => command(
"set-camera-viewport-start", [spatialViewportStart],
),
set_camera_max_orbital_radius: (maxOrbitalRadius) => command(
"set-camera-max-orbital-radius", [maxOrbitalRadius],
),
};
return { facade, dispose, notify };
}
@@ -0,0 +1,115 @@
import type { WebViewer } from "@rerun-io/web-viewer";
import type { RerunFrameApi, RerunNotify } from "./recordedRerunProtocol";
import { createRecordedRerunCameraJournal } from "./recordedRerunCameraJournal";
/** Lives entirely inside the disposable iframe, including pending SDK starts. */
export function createRecordedRerunOwner(create: () => WebViewer, mount: HTMLElement): RerunFrameApi {
let native: WebViewer | null = null;
let notify: RerunNotify | null = null;
const subscriptions = new Map<number, () => void>();
const channels = new Map<number, ReturnType<WebViewer["open_channel"]>>();
let cameraJournal: ReturnType<typeof createRecordedRerunCameraJournal> | null = null;
const send = (message: object) => notify?.(JSON.stringify(message));
const stop = () => {
notify = null;
for (const unsubscribe of subscriptions.values()) {
try { unsubscribe(); } catch { /* Continue partial-start cleanup. */ }
}
subscriptions.clear();
for (const channel of channels.values()) {
try { channel.close(); } catch { /* Other channels must still close. */ }
}
channels.clear();
cameraJournal?.dispose();
cameraJournal = null;
const current = native;
native = null;
try { current?.stop(); } catch { /* Realm teardown remains authoritative. */ }
};
const required = () => {
if (!native) throw new Error("Recorded viewer was disposed");
return native;
};
const start = async (id: number, args: [string | string[] | null, Parameters<WebViewer["start"]>[2]]) => {
const starting = required();
try {
await starting.start(args[0], mount, args[1]);
if (native === starting && starting.canvas) {
cameraJournal?.dispose();
cameraJournal = createRecordedRerunCameraJournal(
starting.canvas,
window as Window & typeof globalThis,
);
}
if (native === starting) send({ type: "started", id });
} catch (error) {
if (native === starting) send({ type: "start-failed", id, message: String(error) });
} finally {
if (native !== starting) {
try { starting.stop(); } catch { /* Late completion after disposal. */ }
}
}
};
return {
version: 2,
connect(callback) {
if (native) throw new Error("Recorded viewer already has an owner");
native = create();
notify = callback;
},
invoke(request, bytes) {
try {
const { method, args = [], id } = JSON.parse(request);
let result: unknown;
switch (method) {
case "stop": stop(); break;
case "start": required(); void start(id, args); break;
case "ready": result = native?.ready ?? false; break;
case "on": {
const subscribe = required().on as (event: string, callback: (...values: unknown[]) => void) => () => void;
subscriptions.set(id, subscribe.call(required(), args[0], (...values) => send({ type: "event", id, values })));
break;
}
case "off":
try { subscriptions.get(id)?.(); } finally { subscriptions.delete(id); }
break;
case "channel-open": {
const name = args[0] as string | undefined;
channels.set(id, required().open_channel(name));
break;
}
case "channel-ready": result = channels.get(id)?.ready ?? false; break;
case "channel-send": if (bytes) {
channels.get(id)?.send_rrd(bytes);
} break;
case "channel-close":
try { channels.get(id)?.close(); } finally {
channels.delete(id);
}
break;
case "configure-camera-journal": cameraJournal?.configure(args[0], args[1]); break;
case "get-camera-eye": result = cameraJournal?.current(); break;
case "set-camera-viewport-start": cameraJournal?.setSpatialViewportStart(args[0]); break;
case "set-camera-max-orbital-radius": cameraJournal?.setMaxOrbitalRadius(args[0]); break;
case "open": required().open(args[0]); break;
case "close": required().close(args[0]); break;
case "override_panel_state": required().override_panel_state(args[0], args[1]); break;
case "get_active_recording_id": result = required().get_active_recording_id(); break;
case "get_active_timeline": result = required().get_active_timeline(args[0]); break;
case "get_current_time": result = required().get_current_time(args[0], args[1]); break;
case "get_playing": result = required().get_playing(args[0]); break;
case "get_time_range": result = required().get_time_range(args[0], args[1]); break;
case "set_active_timeline": required().set_active_timeline(args[0], args[1]); break;
case "set_current_time": required().set_current_time(args[0], args[1], args[2]); break;
case "set_playing": required().set_playing(args[0], args[1]); break;
default: throw new Error("Unsupported recorded viewer command");
}
return JSON.stringify({ result });
} catch (error) {
// A native Error also has a foreign prototype. Return text, never throw
// that object into a parent Promise/callback retained after disposal.
return JSON.stringify({ error: String(error) });
}
},
};
}
@@ -0,0 +1,12 @@
/** Only parent-owned inputs and primitive strings cross the recorded realm.
* An SDK instance, Promise, DOM node or native callback never becomes a value
* retained by React. This is not a second playback clock.
*/
export type RerunInvoke = (request: string, bytes?: Uint8Array) => string;
export type RerunNotify = (message: string) => void;
export interface RerunFrameApi {
version: 2;
connect: (notify: RerunNotify) => void;
invoke: RerunInvoke;
}
export type RerunFrameWindow = Window & { missionCoreRerun?: RerunFrameApi };
@@ -23,26 +23,34 @@ export async function resolveCanonicalLabReplay(
origin = window.location.origin,
signal,
fetcher = globalThis.fetch,
sourceKind = "legacy-vegetation",
}: {
origin?: string;
signal?: AbortSignal;
fetcher?: typeof globalThis.fetch;
sourceKind?: "legacy-vegetation" | "portable-tgs" | "portable-semantic" | "portable-objects";
} = {},
): Promise<CanonicalLabReplayDescriptor> {
const base = new URL(origin);
if (
!SAFE_RESULT_ID.test(resultId)
!(sourceKind === "portable-tgs"
? /^m49-tgs-portable-review-[a-f0-9]{64}$/.test(resultId)
: sourceKind === "portable-semantic"
? /^(?:lab-v1-eomt-ddrnet|ai-layer-(?:ddrnet|eomt))-[a-f0-9]{64}$/.test(resultId)
: sourceKind === "portable-objects"
? /^ai-layer-(?:rf-detr|object-distance)-[a-f0-9]{64}$/.test(resultId)
: SAFE_RESULT_ID.test(resultId))
|| !SAFE_SESSION_SOURCE.test(launch.sourceUrl)
|| launch.viewerSourceUrl !== `${launch.sourceUrl}?generation=${launch.sha256}`
|| !/^[a-f0-9]{64}$/.test(launch.sha256)
) {
throw new Error("Канонический replay LAB имеет небезопасный descriptor.");
}
const sourceUrl =
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}`
+ "/canonical-replay.rrd";
const sourceUrl = sourceKind !== "legacy-vegetation"
? `/api/v1/observatory/portable-results/${resultId}/replays/${launch.sha256}/recording.rrd`
: `/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/canonical-replay.rrd`;
const descriptorUrl = new URL(sourceUrl, `${base.origin}/`);
descriptorUrl.searchParams.set("base_generation", launch.sha256);
if (sourceKind === "legacy-vegetation") descriptorUrl.searchParams.set("base_generation", launch.sha256);
if (descriptorUrl.origin !== base.origin) {
throw new Error("Канонический replay LAB должен быть same-origin.");
}
@@ -74,3 +82,53 @@ export async function resolveCanonicalLabReplay(
blueprintSourceUrl: launch.sourceUrl.replace(/\/recording\.rrd$/, "/blueprint.rrd"),
};
}
export async function resolveAICompositionReplay(
runId: string,
launch: ObservationSessionReplayLaunch,
{
origin = window.location.origin,
signal,
fetcher = globalThis.fetch,
}: {
origin?: string;
signal?: AbortSignal;
fetcher?: typeof globalThis.fetch;
} = {},
): Promise<CanonicalLabReplayDescriptor> {
const base = new URL(origin);
if (
!/^ai-composition-[a-f0-9]{64}$/.test(runId)
|| !SAFE_SESSION_SOURCE.test(launch.sourceUrl)
|| launch.viewerSourceUrl !== `${launch.sourceUrl}?generation=${launch.sha256}`
|| !/^[a-f0-9]{64}$/.test(launch.sha256)
) {
throw new Error("Составной replay LAB имеет небезопасный descriptor.");
}
const sourceUrl = `/api/v1/observatory/ai-composition-runs/${runId}/replays/${launch.sha256}/recording.rrd`;
const descriptorUrl = new URL(sourceUrl, `${base.origin}/`);
const response = await fetcher(descriptorUrl.href, {
method: "HEAD",
credentials: "same-origin",
headers: { Accept: "application/vnd.rerun.rrd" },
signal,
});
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
const byteLength = Number(response.headers.get("Content-Length"));
const sha256 = response.headers.get("ETag")?.match(/^"([a-f0-9]{64})"$/)?.[1];
if (
response.status !== 200 || contentType !== "application/vnd.rerun.rrd"
|| response.headers.get("X-Rerun-Format") !== "RRF2" || !sha256
|| !Number.isSafeInteger(byteLength) || byteLength < 4
|| byteLength > MAX_CANONICAL_REPLAY_BYTES
) {
throw new Error("Составной replay LAB не прошёл проверку.");
}
return {
sourceUrl,
viewerSourceUrl: `${sourceUrl}?generation=${sha256}`,
byteLength,
sha256,
blueprintSourceUrl: launch.sourceUrl.replace(/\/recording\.rrd$/, "/blueprint.rrd"),
};
}
@@ -1026,7 +1026,7 @@ export async function fetchVegetationRouteTgsAnchor(
};
}
export async function fetchVegetationShadowResult(
export async function fetchVegetationShadowResultMetadata(
resultId: string,
{
fetcher = fetch,
@@ -1048,6 +1048,17 @@ export async function fetchVegetationShadowResult(
resultId,
"/api/v1/laboratory/vegetation-shadow",
);
return result;
}
export async function fetchVegetationShadowResult(
resultId: string,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<VegetationShadowResult> {
const result = await fetchVegetationShadowResultMetadata(resultId, { fetcher, signal });
if (!result.routeFullReview) return result;
const timelineResponse = await fetcher(
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}/route-timeline`,
@@ -0,0 +1,140 @@
export const OBSERVATION_LAB_CALCULATION_PROFILE_SCHEMA =
"missioncore.observatory-calculation-profile/v1" as const;
export type ObservationLabCalculationProfileOrigin =
| "archived-definition"
| "existing-result";
export interface ObservationLabCalculationProfile {
readonly schemaVersion: typeof OBSERVATION_LAB_CALCULATION_PROFILE_SCHEMA;
readonly setupId: string;
readonly displayName: string;
readonly origin: ObservationLabCalculationProfileOrigin;
readonly definitionId: string | null;
readonly definitionVersion: number | null;
readonly definitionSha256: string | null;
}
export class ObservationLabCalculationProfileContractError extends Error {
constructor(message: string) {
super(message);
this.name = "ObservationLabCalculationProfileContractError";
}
}
const PROFILE_KEYS = new Set([
"schema_version",
"setup_id",
"display_name",
"origin",
"definition_id",
"definition_version",
"definition_sha256",
]);
const IDENTIFIER = /^[a-z][a-z0-9-]{2,95}$/;
const SHA256 = /^[a-f0-9]{64}$/;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function requireText(value: unknown, field: string, maximum: number): string {
if (
typeof value !== "string"
|| value.length === 0
|| value.length > maximum
|| value !== value.trim()
) {
throw new ObservationLabCalculationProfileContractError(
`Поле calculation_profile.${field} должно быть непустой строкой.`,
);
}
return value;
}
function requireIdentifier(value: unknown, field: string): string {
const identifier = requireText(value, field, 96);
if (!IDENTIFIER.test(identifier)) {
throw new ObservationLabCalculationProfileContractError(
`Поле calculation_profile.${field} содержит некорректный идентификатор.`,
);
}
return identifier;
}
export function decodeObservationLabCalculationProfile(
value: unknown,
sessionId: string,
): ObservationLabCalculationProfile | null {
if (value === null) return null;
if (!isRecord(value) || Object.keys(value).some((key) => !PROFILE_KEYS.has(key))) {
throw new ObservationLabCalculationProfileContractError(
`Профиль расчёта LAB-сессии ${sessionId} нарушил контракт полей.`,
);
}
if (
Object.keys(value).length !== PROFILE_KEYS.size
|| value.schema_version !== OBSERVATION_LAB_CALCULATION_PROFILE_SCHEMA
) {
throw new ObservationLabCalculationProfileContractError(
`Профиль расчёта LAB-сессии ${sessionId} имеет неизвестную версию.`,
);
}
const origin = value.origin;
if (origin !== "archived-definition" && origin !== "existing-result") {
throw new ObservationLabCalculationProfileContractError(
`Профиль расчёта LAB-сессии ${sessionId} имеет неизвестное происхождение.`,
);
}
const definitionId = value.definition_id === null
? null
: requireIdentifier(value.definition_id, "definition_id");
const definitionVersion = value.definition_version === null
? null
: value.definition_version;
if (
definitionVersion !== null
&& (
typeof definitionVersion !== "number"
|| !Number.isInteger(definitionVersion)
|| definitionVersion < 1
)
) {
throw new ObservationLabCalculationProfileContractError(
`Профиль расчёта LAB-сессии ${sessionId} содержит некорректную версию определения.`,
);
}
const definitionSha256 = value.definition_sha256 === null
? null
: requireText(value.definition_sha256, "definition_sha256", 64);
if (definitionSha256 !== null && !SHA256.test(definitionSha256)) {
throw new ObservationLabCalculationProfileContractError(
`Профиль расчёта LAB-сессии ${sessionId} не содержит SHA-256 определения.`,
);
}
const definitionFieldCount = [
definitionId,
definitionVersion,
definitionSha256,
].filter((entry) => entry !== null).length;
if (
(origin === "existing-result" && definitionFieldCount !== 0)
|| (origin === "archived-definition" && definitionFieldCount !== 3)
) {
throw new ObservationLabCalculationProfileContractError(
`Профиль расчёта LAB-сессии ${sessionId} содержит неполную идентичность определения.`,
);
}
return {
schemaVersion: OBSERVATION_LAB_CALCULATION_PROFILE_SCHEMA,
setupId: requireIdentifier(value.setup_id, "setup_id"),
displayName: requireText(value.display_name, "display_name", 256),
origin,
definitionId,
definitionVersion,
definitionSha256,
};
}
@@ -0,0 +1,280 @@
export interface ObservationCanonicalLabReplayCapability {
schemaVersion: "missioncore.observation-lab-replay-capability/v1";
kind: "canonical-recorded-rerun";
viewerProfile: "recorded-session";
timeline: "session_time";
activation: "explicit";
commandsEnabled: false;
}
export interface ObservationPortableLabReplayCapability {
schemaVersion: "missioncore.observation-lab-replay-capability/v2";
kind: "portable-result-review";
viewerProfile: "portable-result";
timeline: "result-defined";
activation: "explicit";
commandsEnabled: false;
}
export type ObservationLabReplayCapability =
| ObservationCanonicalLabReplayCapability
| ObservationPortableLabReplayCapability;
export class ObservationLabReplayCapabilityContractError extends Error {
constructor(message: string) {
super(message);
this.name = "ObservationLabReplayCapabilityContractError";
}
}
const SHA256 = /^[a-f0-9]{64}$/;
const CAPABILITY_KEYS = new Set([
"schema_version",
"kind",
"viewer_profile",
"timeline",
"activation",
"commands_enabled",
]);
const CANONICAL_PROVENANCE_KEYS = new Set([
"schema_version",
"evidence_identity_sha256",
"result_document_sha256",
"replay_capability",
"authority",
"method",
]);
const PORTABLE_PROVENANCE_KEYS = new Set([
"schema_version",
"authority",
"calculation_profile",
"calculation_profile_sha256",
"job",
"source",
"run_definition",
"result_package",
"replay_capability",
"storage",
"method",
]);
const AUTHORITY_KEYS = new Set([
"commands_enabled",
"navigation_or_safety_accepted",
"actuation_accepted",
]);
const METHOD_KEYS = new Set([
"schema_version",
"completeness",
"execution_class",
"pipeline_id",
"components",
]);
const METHOD_COMPONENT_KEYS = new Set([
"kind",
"name",
"version",
"role",
"identity_sha256",
]);
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function assertExactKeys(
value: Record<string, unknown>,
expected: ReadonlySet<string>,
label: string,
): void {
const keys = Object.keys(value);
if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) {
throw new ObservationLabReplayCapabilityContractError(
`${label} содержит неизвестные или пропущенные поля.`,
);
}
}
export function decodeLabReplayCapability(
value: unknown,
sessionId: string,
): ObservationLabReplayCapability | null {
if (value === null || value === undefined) return null;
if (!isRecord(value)) {
throw new ObservationLabReplayCapabilityContractError(
`Replay-возможность LAB-сессии ${sessionId} должна быть объектом или null.`,
);
}
assertExactKeys(value, CAPABILITY_KEYS, `Replay-возможность LAB-сессии ${sessionId}`);
if (
value.activation !== "explicit"
|| value.commands_enabled !== false
) {
throw new ObservationLabReplayCapabilityContractError(
`Replay-возможность LAB-сессии ${sessionId} нарушает observation-only контракт.`,
);
}
if (
value.schema_version === "missioncore.observation-lab-replay-capability/v1"
&& value.kind === "canonical-recorded-rerun"
&& value.viewer_profile === "recorded-session"
&& value.timeline === "session_time"
) {
return {
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
kind: "canonical-recorded-rerun",
viewerProfile: "recorded-session",
timeline: "session_time",
activation: "explicit",
commandsEnabled: false,
};
}
if (
value.schema_version === "missioncore.observation-lab-replay-capability/v2"
&& value.kind === "portable-result-review"
&& value.viewer_profile === "portable-result"
&& value.timeline === "result-defined"
) {
return {
schemaVersion: "missioncore.observation-lab-replay-capability/v2",
kind: "portable-result-review",
viewerProfile: "portable-result",
timeline: "result-defined",
activation: "explicit",
commandsEnabled: false,
};
}
throw new ObservationLabReplayCapabilityContractError(
`Replay-возможность LAB-сессии ${sessionId} содержит неизвестный viewer capability.`,
);
}
export function decodeCanonicalLabReplayCapabilityProvenance(
provenance: Readonly<Record<string, unknown>>,
sessionId: string,
resultId: string,
): ObservationCanonicalLabReplayCapability {
assertExactKeys(provenance, CANONICAL_PROVENANCE_KEYS, `Canonical provenance LAB-сессии ${sessionId}`);
if (
provenance.schema_version !== "missioncore.canonical-recorded-lab-projection/v1"
|| typeof provenance.evidence_identity_sha256 !== "string"
|| !SHA256.test(provenance.evidence_identity_sha256)
|| resultId !== `lab-v1-vegetation-shadow-${provenance.evidence_identity_sha256}`
|| typeof provenance.result_document_sha256 !== "string"
|| !SHA256.test(provenance.result_document_sha256)
) {
throw new ObservationLabReplayCapabilityContractError(
`Canonical provenance LAB-сессии ${sessionId} потеряла immutable identity.`,
);
}
if (!isRecord(provenance.authority)) {
throw new ObservationLabReplayCapabilityContractError(
`Canonical provenance LAB-сессии ${sessionId} не содержит authority.`,
);
}
assertExactKeys(provenance.authority, AUTHORITY_KEYS, `Canonical authority LAB-сессии ${sessionId}`);
if (
provenance.authority.commands_enabled !== false
|| provenance.authority.navigation_or_safety_accepted !== false
|| provenance.authority.actuation_accepted !== false
) {
throw new ObservationLabReplayCapabilityContractError(
`Canonical provenance LAB-сессии ${sessionId} нарушает observation-only authority.`,
);
}
if (!isRecord(provenance.method)) {
throw new ObservationLabReplayCapabilityContractError(
`Canonical provenance LAB-сессии ${sessionId} не содержит method manifest.`,
);
}
assertExactKeys(provenance.method, METHOD_KEYS, `Canonical method LAB-сессии ${sessionId}`);
if (
provenance.method.schema_version !== "missioncore.laboratory-method/v1"
|| provenance.method.completeness !== "legacy-partial"
|| provenance.method.execution_class !== "ai-inference"
|| provenance.method.pipeline_id
!== "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1"
|| !Array.isArray(provenance.method.components)
|| provenance.method.components.length !== 1
) {
throw new ObservationLabReplayCapabilityContractError(
`Canonical method LAB-сессии ${sessionId} не соответствует записанному прогону.`,
);
}
const component = provenance.method.components[0];
if (!isRecord(component)) {
throw new ObservationLabReplayCapabilityContractError(
`Canonical method LAB-сессии ${sessionId} не содержит source component.`,
);
}
assertExactKeys(
component,
METHOD_COMPONENT_KEYS,
`Canonical source component LAB-сессии ${sessionId}`,
);
if (
component.kind !== "source"
|| component.name !== "sealed full-route LAB result"
|| component.version !== "missioncore.lab-v1-vegetation-shadow/v1"
|| component.role !== "immutable Session catalog projection"
|| component.identity_sha256 !== provenance.evidence_identity_sha256
) {
throw new ObservationLabReplayCapabilityContractError(
`Canonical source component LAB-сессии ${sessionId} потерял immutable identity.`,
);
}
const capability = decodeLabReplayCapability(provenance.replay_capability, sessionId);
if (capability === null || capability.kind !== "canonical-recorded-rerun") {
throw new ObservationLabReplayCapabilityContractError(
`Canonical provenance LAB-сессии ${sessionId} не содержит replay capability.`,
);
}
return capability;
}
export function decodePortableLabReplayCapabilityProvenance(
provenance: Readonly<Record<string, unknown>>,
sessionId: string,
resultId: string,
sourceSessionId: string,
definitionSha256: string,
): ObservationPortableLabReplayCapability {
assertExactKeys(
provenance,
PORTABLE_PROVENANCE_KEYS,
`Portable provenance LAB-сессии ${sessionId}`,
);
if (
provenance.schema_version !== "missioncore.observatory-portable-result-publication/v1"
|| !isRecord(provenance.source)
|| provenance.source.session_id !== sourceSessionId
|| !isRecord(provenance.run_definition)
|| provenance.run_definition.definition_sha256 !== definitionSha256
|| !isRecord(provenance.result_package)
|| typeof provenance.result_package.manifest_sha256 !== "string"
|| !SHA256.test(provenance.result_package.manifest_sha256)
|| typeof provenance.result_package.artifact_manifest_id !== "string"
|| !SHA256.test(provenance.result_package.artifact_manifest_id)
|| sessionId !== resultId
) {
throw new ObservationLabReplayCapabilityContractError(
`Portable provenance LAB-сессии ${sessionId} потеряла immutable identity.`,
);
}
const capability = decodeLabReplayCapability(provenance.replay_capability, sessionId);
if (capability === null || capability.kind !== "portable-result-review") {
throw new ObservationLabReplayCapabilityContractError(
`Portable provenance LAB-сессии ${sessionId} не содержит viewer capability.`,
);
}
return capability;
}
/** Rolling bridge for a new frontend against the pre-v2 catalog endpoint. */
export function decodeRollingCanonicalLabReplayCapability(
provenance: Readonly<Record<string, unknown>>,
sessionId: string,
resultId: string,
): ObservationLabReplayCapability | null {
if (!Object.prototype.hasOwnProperty.call(provenance, "replay_capability")) return null;
return decodeCanonicalLabReplayCapabilityProvenance(provenance, sessionId, resultId);
}
@@ -0,0 +1,53 @@
/** Small lease for the server's ephemeral blueprint, never for the recording. */
export function keepRecordedBlueprintSession({
endpointUrl,
origin,
applicationId,
recordingId,
ownerId,
fetcher = fetch,
schedule = (callback: () => void) => window.setInterval(callback, 30_000),
cancel = (handle: number) => window.clearInterval(handle),
}: {
endpointUrl: string;
origin: string;
applicationId: string;
recordingId: string;
ownerId: string;
fetcher?: typeof fetch;
schedule?: (callback: () => void) => number;
cancel?: (handle: number) => void;
}): () => void {
const endpoint = new URL(endpointUrl, origin);
if (endpoint.origin !== origin || endpoint.search || endpoint.hash ||
!/^\/api\/v1\/observation-sessions\/[A-Za-z0-9._:-]+\/blueprint\.rrd$/.test(endpoint.pathname)) {
throw new Error("Unsafe recorded blueprint lifecycle endpoint");
}
endpoint.pathname = endpoint.pathname.replace(/blueprint\.rrd$/, "blueprint-lifecycle");
const identity = { application_id: applicationId, recording_id: recordingId,
blueprint_session_id: ownerId };
let closed = false;
let pending: AbortController | null = null;
const renew = () => {
if (closed) return;
pending?.abort();
pending = new AbortController();
void fetcher(endpoint.href, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...identity, action: "renew" }), signal: pending.signal,
}).catch(() => { /* The TTL cleans up after a lost browser/network. */ });
};
const timer = schedule(renew);
return () => {
if (closed) return;
closed = true;
cancel(timer);
pending?.abort();
pending = null;
void fetcher(endpoint.href, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...identity, action: "release" }),
keepalive: true, signal: AbortSignal.timeout(5_000),
}).catch(() => { /* Terminal release is best-effort; the server also has TTL. */ });
};
}
@@ -0,0 +1,43 @@
export interface ObservationReplayAttempt {
readonly signal: AbortSignal;
isCurrent: () => boolean;
finish: () => boolean;
}
export interface ObservationReplayCoordinator {
begin: () => ObservationReplayAttempt;
cancel: () => void;
}
/** Latest explicit admission owns the viewer, even if an older promise settles later. */
export function createObservationReplayCoordinator(): ObservationReplayCoordinator {
let sequence = 0;
let active: AbortController | null = null;
return {
begin() {
active?.abort();
const controller = new AbortController();
const attemptSequence = ++sequence;
active = controller;
return {
signal: controller.signal,
isCurrent: () => (
!controller.signal.aborted
&& active === controller
&& sequence === attemptSequence
),
finish: () => {
if (active !== controller || sequence !== attemptSequence) return false;
active = null;
return true;
},
};
},
cancel() {
active?.abort();
// Keep settlement ownership until finish(). isCurrent() already fails,
// while begin() can replace this attempt immediately.
},
};
}
@@ -1,6 +1,20 @@
import {
MAX_RECORDED_CAMERA_SOURCES,
} from "./recordedSessionAdmission";
import {
decodeLabReplayCapability,
decodeRollingCanonicalLabReplayCapability,
ObservationLabReplayCapabilityContractError,
type ObservationLabReplayCapability,
} from "./labReplayCapability";
import {
decodeObservationLabCalculationProfile,
ObservationLabCalculationProfileContractError,
type ObservationLabCalculationProfile,
} from "./labCalculationProfile";
export type { ObservationLabReplayCapability } from "./labReplayCapability";
export type { ObservationLabCalculationProfile } from "./labCalculationProfile";
export type ObservationSessionStatus =
| "recording"
@@ -20,6 +34,8 @@ export interface ObservationLabInstance {
configSha256: string | null;
runCreatedAtUtc: string;
publishedAtUtc: string;
replayCapability: ObservationLabReplayCapability | null;
calculationProfile: ObservationLabCalculationProfile | null;
provenance: Readonly<Record<string, unknown>>;
}
@@ -32,10 +48,21 @@ export interface ObservationSessionSummary {
modalities: readonly string[];
durationSeconds: number;
replayable: boolean;
captureAttestation: ObservationCaptureAttestation | null;
preparation: ObservationSessionCatalogPreparation | null;
lab: ObservationLabInstance | null;
}
export interface ObservationCaptureAttestation {
readonly equipmentModelId: string;
readonly equipmentModelSha256: string;
readonly equipmentDisplayName: string;
readonly captureProfileId: string;
readonly captureProfileSha256: string;
readonly captureAttestationSha256: string;
readonly state: "attested" | "attested-with-legacy-evidence" | "unknown";
}
export interface ObservationSessionCatalog {
items: readonly ObservationSessionSummary[];
}
@@ -152,10 +179,11 @@ const ITEM_KEYS = new Set([
"modalities",
"duration_seconds",
"replayable",
"capture_attestation",
"preparation",
"lab",
]);
const LAB_KEYS = new Set([
const LEGACY_LAB_KEYS = new Set([
"lab_id",
"source_session_id",
"result_kind",
@@ -166,6 +194,27 @@ const LAB_KEYS = new Set([
"published_at_utc",
"provenance",
]);
const LAB_V2_KEYS = new Set([...LEGACY_LAB_KEYS, "replay_capability"]);
const LAB_V3_KEYS = new Set([
...LAB_V2_KEYS,
"calculation_profile",
]);
const CAPTURE_ATTESTATION_KEYS = new Set([
"schema_version",
"equipment_model",
"capture_profile",
"capture_attestation_sha256",
"state",
]);
const EQUIPMENT_MODEL_KEYS = new Set([
"equipment_model_id",
"equipment_model_sha256",
"display_name",
]);
const CAPTURE_PROFILE_KEYS = new Set([
"capture_profile_id",
"capture_profile_sha256",
]);
const CATALOG_PREPARATION_KEYS = new Set([
"preparation_id",
"state",
@@ -379,11 +428,103 @@ function decodeItem(value: unknown, index: number): ObservationSessionSummary {
modalities: requireModalities(value.modalities),
durationSeconds: value.duration_seconds,
replayable: value.replayable,
captureAttestation: decodeCaptureAttestation(value.capture_attestation, id),
preparation: decodeCatalogPreparation(value.preparation, id),
lab: decodeLabInstance(value.lab, id),
};
}
function decodeCaptureAttestation(
value: unknown,
sessionId: string,
): ObservationCaptureAttestation | null {
if (value === null || value === undefined) return null;
if (!isRecord(value)) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} должна быть объектом или null.`,
);
}
assertExactKeys(value, CAPTURE_ATTESTATION_KEYS, `Аттестация записи ${sessionId}`);
if (value.schema_version !== "missioncore.recorded-capture-attestation/v1") {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} имеет неизвестную схему.`,
);
}
if (!isRecord(value.equipment_model) || !isRecord(value.capture_profile)) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} не содержит оборудование или профиль записи.`,
);
}
assertExactKeys(
value.equipment_model,
EQUIPMENT_MODEL_KEYS,
`Оборудование записи ${sessionId}`,
);
assertExactKeys(
value.capture_profile,
CAPTURE_PROFILE_KEYS,
`Профиль записи ${sessionId}`,
);
const equipmentModelId = requireString(
value.equipment_model.equipment_model_id,
"equipment_model_id",
128,
);
const equipmentModelSha256 = requireString(
value.equipment_model.equipment_model_sha256,
"equipment_model_sha256",
64,
);
const captureProfileId = requireString(
value.capture_profile.capture_profile_id,
"capture_profile_id",
128,
);
const captureProfileSha256 = requireString(
value.capture_profile.capture_profile_sha256,
"capture_profile_sha256",
64,
);
const captureAttestationSha256 = requireString(
value.capture_attestation_sha256,
"capture_attestation_sha256",
64,
);
if (
!SAFE_ID.test(equipmentModelId)
|| !SAFE_ID.test(captureProfileId)
|| !SHA256.test(equipmentModelSha256)
|| !SHA256.test(captureProfileSha256)
|| !SHA256.test(captureAttestationSha256)
) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} содержит некорректную immutable identity.`,
);
}
if (
value.state !== "attested"
&& value.state !== "attested-with-legacy-evidence"
&& value.state !== "unknown"
) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} содержит неизвестное состояние.`,
);
}
return {
equipmentModelId,
equipmentModelSha256,
equipmentDisplayName: requireString(
value.equipment_model.display_name,
"equipment display_name",
128,
),
captureProfileId,
captureProfileSha256,
captureAttestationSha256,
state: value.state,
};
}
function decodeLabInstance(
value: unknown,
sessionId: string,
@@ -394,7 +535,23 @@ function decodeLabInstance(
`LAB-привязка сессии ${sessionId} должна быть объектом или null.`,
);
}
assertExactKeys(value, LAB_KEYS, `LAB-привязка сессии ${sessionId}`);
const hasTypedCapability = Object.prototype.hasOwnProperty.call(
value,
"replay_capability",
);
const hasCalculationProfile = Object.prototype.hasOwnProperty.call(
value,
"calculation_profile",
);
assertExactKeys(
value,
hasCalculationProfile
? LAB_V3_KEYS
: hasTypedCapability
? LAB_V2_KEYS
: LEGACY_LAB_KEYS,
`LAB-привязка сессии ${sessionId}`,
);
const labId = requireString(value.lab_id, `lab(${sessionId}).lab_id`, 36);
if (!/^LAB [A-Z][A-Z0-9._-]{0,31}$/.test(labId)) {
throw new ObservationSessionContractError("Каталог содержит некорректный LAB-маркер.");
@@ -430,6 +587,28 @@ function decodeLabInstance(
if (!isRecord(value.provenance)) {
throw new ObservationSessionContractError("LAB provenance должен быть JSON-объектом.");
}
let replayCapability: ObservationLabReplayCapability | null;
try {
replayCapability = hasTypedCapability
? decodeLabReplayCapability(value.replay_capability, sessionId)
: decodeRollingCanonicalLabReplayCapability(value.provenance, sessionId, resultId);
} catch (error) {
if (error instanceof ObservationLabReplayCapabilityContractError) {
throw new ObservationSessionContractError(error.message);
}
throw error;
}
let calculationProfile: ObservationLabCalculationProfile | null;
try {
calculationProfile = hasCalculationProfile
? decodeObservationLabCalculationProfile(value.calculation_profile, sessionId)
: null;
} catch (error) {
if (error instanceof ObservationLabCalculationProfileContractError) {
throw new ObservationSessionContractError(error.message);
}
throw error;
}
return {
labId,
sourceSessionId,
@@ -445,6 +624,8 @@ function decodeLabInstance(
value.published_at_utc,
`lab(${sessionId}).published_at_utc`,
),
replayCapability,
calculationProfile,
provenance: value.provenance,
};
}
@@ -1181,6 +1362,7 @@ export async function fetchObservationSessionCatalog({
queryParameters.set("limit", String(Number(limit)));
}
if (scope !== "all") queryParameters.set("scope", scope);
if (scope === "laboratory") queryParameters.set("lab_contract", "v3");
const serializedQuery = queryParameters.toString();
const query = serializedQuery ? `?${serializedQuery}` : "";
let response: Response;
@@ -0,0 +1,72 @@
import {
decodeObservationSessionCatalog,
ObservationSessionApiError,
ObservationSessionContractError,
type ObservationSessionCatalog,
type ObservationSessionFetch,
type ObservationSessionScope,
} from "./sessionArchive";
const PAGE_SCHEMA = "missioncore.observation-session-page/v1";
const CURSOR = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
export interface ObservationSessionCatalogPage extends ObservationSessionCatalog {
readonly nextCursor: string | null;
}
/** Opt-in paging leaves the legacy catalog response and consumers unchanged. */
export async function fetchObservationSessionCatalogPage({
scope,
limit = 100,
cursor = null,
signal,
fetcher = globalThis.fetch,
}: {
scope: ObservationSessionScope;
limit?: number;
cursor?: string | null;
signal?: AbortSignal;
fetcher?: ObservationSessionFetch;
}): Promise<ObservationSessionCatalogPage> {
if (!Number.isInteger(limit) || limit < 1 || limit > 100
|| (cursor !== null && !CURSOR.test(cursor))) {
throw new ObservationSessionContractError("Некорректная страница каталога сессий.");
}
signal?.throwIfAborted();
const query = new URLSearchParams({ limit: String(limit), scope });
if (scope === "laboratory") query.set("lab_contract", "v3");
query.set("pagination", "cursor-v1");
if (cursor !== null) query.set("cursor", cursor);
const response = await fetcher(`/api/v1/observation-sessions?${query}`, {
method: "GET", headers: { Accept: "application/json" }, signal,
});
if (!response.ok) {
throw new ObservationSessionApiError(
`Не удалось загрузить страницу каталога сессий (HTTP ${response.status}).`,
response.status,
);
}
let body: unknown;
try {
body = await response.json();
} catch {
throw new ObservationSessionContractError("Некорректный ответ каталога сессий.");
}
if (typeof body !== "object" || body === null || Array.isArray(body)) {
throw new ObservationSessionContractError("Страница каталога должна быть объектом.");
}
const page = body as Record<string, unknown>;
if (Object.keys(page).length !== 3
|| page.schema_version !== PAGE_SCHEMA
|| !Object.hasOwn(page, "items")
|| !(page.next_cursor === null
|| (typeof page.next_cursor === "string" && CURSOR.test(page.next_cursor)))) {
throw new ObservationSessionContractError("Нарушен контракт страницы каталога сессий.");
}
const catalog = decodeObservationSessionCatalog({ items: page.items });
if (catalog.items.length > limit) {
throw new ObservationSessionContractError("Страница каталога превысила запрошенный размер.");
}
signal?.throwIfAborted();
return { ...catalog, nextCursor: page.next_cursor };
}
@@ -12,6 +12,16 @@ import {
type ObservationSessionScope,
type ObservationSessionSummary,
} from "./sessionArchive";
import {
createObservationReplayCoordinator,
type ObservationReplayCoordinator,
} from "./replayCoordinator";
export {
createObservationReplayCoordinator,
type ObservationReplayAttempt,
type ObservationReplayCoordinator,
} from "./replayCoordinator";
export type ObservationSessionsLoadState = "idle" | "loading" | "ready" | "error";
export type ObservationReplayOutcome = "accepted" | "error" | "cancelled";
@@ -41,17 +51,6 @@ export interface ObservationSessionsController {
remove: (sessionId: string) => Promise<boolean>;
}
export interface ObservationReplayAttempt {
readonly signal: AbortSignal;
isCurrent: () => boolean;
finish: () => boolean;
}
export interface ObservationReplayCoordinator {
begin: () => ObservationReplayAttempt;
cancel: () => void;
}
export async function deleteObservationSessionAfterTeardown(
sessionId: string,
{
@@ -93,41 +92,6 @@ export class ObservationPreparationStalledError extends Error {
}
}
/** Latest selection wins, even if an obsolete server job finishes later. */
export function createObservationReplayCoordinator(): ObservationReplayCoordinator {
let sequence = 0;
let active: AbortController | null = null;
return {
begin() {
active?.abort();
const controller = new AbortController();
const attemptSequence = ++sequence;
active = controller;
return {
signal: controller.signal,
isCurrent: () => (
!controller.signal.aborted &&
active === controller &&
sequence === attemptSequence
),
finish: () => {
if (active !== controller || sequence !== attemptSequence) return false;
active = null;
return true;
},
};
},
cancel() {
active?.abort();
// Keep ownership until the cancelled attempt reaches `finish()`. This
// lets its finally block settle a replacement that already passed
// onReplayBegin, while `isCurrent()` still fails immediately because the
// signal is aborted. A later `begin()` replaces and invalidates it.
},
};
}
function errorMessage(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
@@ -24,9 +24,11 @@ export interface RerunPlaybackController {
export interface RecordedPerceptionLayers {
enabled: boolean;
cameraImage?: boolean;
detections2d: boolean;
segmentation: boolean;
cuboids3d: boolean;
costmap?: boolean;
}
export interface RecordedRrdArtifactDescriptor {
@@ -75,6 +77,8 @@ export interface RecordedSessionRerunProfile {
semanticLayer?: "city" | "vegetation";
/** Keeps one native Rerun store/viewer while presenting the accepted two-pane LAB layout. */
unifiedPerception?: boolean;
/** Controlled camera-column share for the native horizontal Rerun container. */
unifiedCameraShare?: number;
/** Requests the canonical top-down eye without changing the world coordinate frame. */
planView?: boolean;
perceptionRetryGeneration: number;
@@ -0,0 +1,347 @@
import {
decodeObservatoryRecordedJob,
type ObservatoryRecordedJob,
} from "./recordedJobs";
const COMPOSITION_SCHEMA = "missioncore.observatory-ai-composition/v1";
const CATALOG_SCHEMA = "missioncore.observatory-ai-module-catalog/v1";
const RECEIPT_SCHEMA = "missioncore.observatory-ai-composition-receipt/v3";
const SHA256 = /^[a-f0-9]{64}$/;
export type AIGroupId = "segmentation" | "detection" | "geometry" | "range" | "motion" | "policy";
export interface AIModule {
readonly moduleId: string;
readonly moduleSha256: string;
readonly label: string;
readonly dockerName: string;
readonly requires: readonly string[];
readonly provides: readonly string[];
readonly defaults: Readonly<Record<string, unknown>>;
}
export interface AIModuleGroup {
readonly group: AIGroupId;
readonly modules: readonly AIModule[];
}
export interface AIModuleCatalog {
readonly groups: readonly AIModuleGroup[];
}
export interface AICompositionReceipt {
readonly compositionSha256: string;
readonly created: boolean;
readonly dispatchReady: boolean;
readonly dispatchReason: string;
readonly setupIds: readonly string[];
readonly jobs: readonly ObservatoryRecordedJob[];
readonly run: AICompositionRun;
}
export interface AIViewerLayer {
readonly layerId: string;
readonly paneId: "camera" | "spatial";
readonly label: string;
readonly control: "toggle" | "toggle-with-settings";
readonly order: number;
}
export interface AICompositionPresentation {
readonly configurationLabel: string;
readonly modules: readonly { readonly moduleId: string; readonly label: string }[];
readonly viewerLayers: readonly AIViewerLayer[];
}
export interface AICompositionRun {
readonly runId: string;
readonly sourceSessionId: string;
readonly compositionSha256: string;
readonly moduleIds: readonly string[];
readonly setupIds: readonly string[];
readonly jobIds: readonly string[];
readonly resultIds: readonly string[];
readonly createdAtUtc: string;
readonly state: "running" | "ready" | "failed";
readonly configurationLabel: string;
readonly displayName: string | null;
readonly presentation: AICompositionPresentation;
readonly jobs: readonly ObservatoryRecordedJob[];
}
export const AI_MODULE_SETUP_IDS: Readonly<Record<string, string>> = Object.freeze({
ddrnet: "ai-segmentation-ddrnet-v1",
eomt: "ai-segmentation-eomt-v1",
tgs: "m49-tgs-portable-v2",
"rf-detr": "ai-detection-rf-detr-v1",
"object-distance": "ai-range-object-distance-v1",
});
export function aiModuleIdForSetup(setupId: string): string | null {
return Object.entries(AI_MODULE_SETUP_IDS).find(([, value]) => value === setupId)?.[0] ?? null;
}
export async function fetchAIModuleCatalog(signal?: AbortSignal): Promise<AIModuleCatalog> {
const response = await fetch("/api/v1/observatory/ai-module-catalog", {
headers: { Accept: "application/json" }, signal,
});
const body = await bodyOf(response);
if (!response.ok) throw apiError(body, response.status);
const root = record(body, "каталог AI-модулей");
exactKeys(root, ["authority", "groups", "schema_version"]);
if (root.schema_version !== CATALOG_SCHEMA) throw new Error("Версия каталога AI-модулей изменилась.");
const known = new Set<AIGroupId>(["segmentation", "detection", "geometry", "range", "motion", "policy"]);
const groups = array(root.groups).map((value): AIModuleGroup => {
const group = record(value, "группа AI-модулей");
exactKeys(group, ["group", "modules"]);
if (typeof group.group !== "string" || !known.has(group.group as AIGroupId)) {
throw new Error("Каталог вернул неизвестную группу AI-модулей.");
}
return { group: group.group as AIGroupId, modules: array(group.modules).map(moduleOf) };
});
if (new Set(groups.map((group) => group.group)).size !== groups.length) {
throw new Error("Каталог AI-модулей содержит повторяющиеся группы.");
}
return { groups };
}
export async function saveAIComposition(
sourceSessionId: string,
selections: readonly { group: AIGroupId; module: AIModule }[],
): Promise<AICompositionReceipt> {
const response = await fetch("/api/v1/observatory/ai-compositions", {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
schema_version: COMPOSITION_SCHEMA,
source_session_id: sourceSessionId,
idempotency_key: createIdempotencyKey(),
selections: selections.map(({ group, module }) => ({
group, module_id: module.moduleId, module_sha256: module.moduleSha256,
parameters: module.defaults,
})),
}),
});
const body = await bodyOf(response);
if (!response.ok) throw apiError(body, response.status);
const root = record(body, "конфигурация AI-слоя");
exactKeys(root, ["composition", "composition_sha256", "created", "dispatch", "run", "schema_version", "source_session_id"]);
if (root.schema_version !== RECEIPT_SCHEMA || root.source_session_id !== sourceSessionId) {
throw new Error("Сервер вернул конфигурацию для другой записи.");
}
const digest = text(root.composition_sha256);
if (!SHA256.test(digest)) throw new Error("Сервер вернул некорректную идентичность композиции.");
const dispatch = record(root.dispatch, "готовность композиции");
exactKeys(dispatch, ["jobs", "ready", "reason", "setup_ids"]);
if (dispatch.ready !== true) {
throw new Error(text(dispatch.reason));
}
const setupIds = array(dispatch.setup_ids).map(text);
const jobs = array(dispatch.jobs).map(decodeObservatoryRecordedJob);
if (!jobs.length || jobs.length !== setupIds.length
|| jobs.some((job, index) => job.setupId !== setupIds[index])) {
throw new Error("Сервер вернул неполную очередь композиции.");
}
const run = compositionRunOf(root.run);
return {
compositionSha256: digest,
created: boolean(dispatch && root.created),
dispatchReady: boolean(dispatch.ready),
dispatchReason: text(dispatch.reason),
setupIds,
jobs,
run,
};
}
export async function fetchAICompositionRuns(
sourceSessionId: string,
signal?: AbortSignal,
): Promise<readonly AICompositionRun[]> {
const query = new URLSearchParams({ source_session_id: sourceSessionId });
const response = await fetch(`/api/v1/observatory/ai-composition-runs?${query}`, {
headers: { Accept: "application/json" }, signal,
});
const body = await bodyOf(response);
if (!response.ok) throw apiError(body, response.status);
const root = record(body, "запуски композиций");
exactKeys(root, ["items", "schema_version"]);
if (root.schema_version !== "missioncore.observatory-ai-composition-run-list/v1") {
throw new Error("Версия запусков AI-композиций изменилась.");
}
return array(root.items).map(compositionRunOf);
}
export async function renameAICompositionRunProjection(
runId: string,
displayName: string,
): Promise<string> {
if (!/^ai-composition-[a-f0-9]{64}$/.test(runId)) {
throw new Error("Некорректная идентичность результата AI inference.");
}
const normalized = displayName.trim();
if (!normalized || normalized.length > 160) {
throw new Error("Название результата должно содержать от 1 до 160 символов.");
}
const response = await fetch(`/api/v1/observatory/ai-composition-runs/${encodeURIComponent(runId)}`, {
method: "PATCH",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
schema_version: "missioncore.observatory-ai-composition-run-rename/v1",
display_name: normalized,
}),
});
const body = await bodyOf(response);
if (!response.ok) throw apiError(body, response.status);
const row = record(body, "результат AI inference");
exactKeys(row, ["display_name", "run_id", "schema_version"]);
if (
row.schema_version !== "missioncore.observatory-ai-composition-run-projection/v1"
|| row.run_id !== runId
|| row.display_name !== normalized
) throw new Error("Сервер не подтвердил переименование результата AI inference.");
return normalized;
}
export async function deleteAICompositionRunProjection(runId: string): Promise<void> {
if (!/^ai-composition-[a-f0-9]{64}$/.test(runId)) {
throw new Error("Некорректная идентичность результата AI inference.");
}
const response = await fetch(`/api/v1/observatory/ai-composition-runs/${encodeURIComponent(runId)}`, {
method: "DELETE",
headers: { Accept: "application/json" },
});
if (!response.ok || response.status !== 204) {
throw apiError(await bodyOf(response), response.status);
}
}
function compositionRunOf(value: unknown): AICompositionRun {
const row = record(value, "запуск AI-композиции");
const expected = [
"composition_sha256", "configuration_label", "created_at_utc", "display_name", "job_ids", "jobs",
"module_ids", "presentation", "result_ids", "run_id", "schema_version", "setup_ids",
"source_session_id", "state",
];
// The receipt embeds the immutable run before its live state projection.
const receiptShape = [
"composition_sha256", "created_at_utc", "job_ids", "module_ids", "presentation",
"run_id", "schema_version", "setup_ids", "source_session_id",
];
const keys = Object.keys(row);
const projected = keys.length === expected.length;
exactKeys(row, projected ? expected : receiptShape);
if (row.schema_version !== "missioncore.observatory-ai-composition-run/v1") {
throw new Error("Версия запуска AI-композиции изменилась.");
}
const sourceSessionId = text(row.source_session_id);
const compositionSha256 = text(row.composition_sha256);
const runId = text(row.run_id);
if (!/^ai-composition-[a-f0-9]{64}$/.test(runId) || !SHA256.test(compositionSha256)) {
throw new Error("Некорректная идентичность запуска AI-композиции.");
}
const presentation = presentationOf(row.presentation);
const state = projected ? text(row.state) : "running";
if (!new Set(["running", "ready", "failed"]).has(state)) {
throw new Error("Неизвестное состояние запуска AI-композиции.");
}
return {
runId,
sourceSessionId,
compositionSha256,
moduleIds: array(row.module_ids).map(text),
setupIds: array(row.setup_ids).map(text),
jobIds: array(row.job_ids).map(text),
resultIds: projected ? array(row.result_ids).map(text) : [],
createdAtUtc: text(row.created_at_utc),
state: state as AICompositionRun["state"],
configurationLabel: projected ? text(row.configuration_label) : presentation.configurationLabel,
displayName: projected
? row.display_name === null ? null : text(row.display_name)
: null,
presentation,
jobs: projected ? array(row.jobs).map(decodeObservatoryRecordedJob) : [],
};
}
function presentationOf(value: unknown): AICompositionPresentation {
const row = record(value, "проекция AI-композиции");
exactKeys(row, ["configuration_label", "modules", "schema_version", "viewer_layers"]);
if (row.schema_version !== "missioncore.observatory-presentation-projection/v1") {
throw new Error("Версия проекции AI-композиции изменилась.");
}
const modules = array(row.modules).map((value) => {
const module = record(value, "модуль проекции");
exactKeys(module, ["label", "module_id"]);
return { moduleId: text(module.module_id), label: text(module.label) };
});
const viewerLayers = array(row.viewer_layers).map((value): AIViewerLayer => {
const layer = record(value, "слой viewer");
exactKeys(layer, ["control", "label", "layer_id", "order", "pane_id"]);
const paneId = text(layer.pane_id);
const control = text(layer.control);
if (!new Set(["camera", "spatial"]).has(paneId)
|| !new Set(["toggle", "toggle-with-settings"]).has(control)
|| typeof layer.order !== "number") {
throw new Error("Некорректная проекция слоя viewer.");
}
return {
layerId: text(layer.layer_id), paneId: paneId as AIViewerLayer["paneId"],
label: text(layer.label), control: control as AIViewerLayer["control"], order: layer.order,
};
});
return { configurationLabel: text(row.configuration_label), modules, viewerLayers };
}
export async function fetchAICompositionJobs(
sourceSessionId: string,
signal?: AbortSignal,
): Promise<readonly ObservatoryRecordedJob[]> {
const query = new URLSearchParams({ source_session_id: sourceSessionId });
const response = await fetch(`/api/v1/observatory/ai-runs?${query}`, {
headers: { Accept: "application/json" }, signal,
});
const body = await bodyOf(response);
if (!response.ok) throw apiError(body, response.status);
const root = record(body, "очередь AI-слоёв");
exactKeys(root, ["authority", "items", "schema_version"]);
if (root.schema_version !== "missioncore.observatory-recorded-job-list/v1") {
throw new Error("Версия очереди AI-слоёв изменилась.");
}
return array(root.items).map(decodeObservatoryRecordedJob);
}
function moduleOf(value: unknown): AIModule {
const row = record(value, "AI-модуль");
exactKeys(row, ["defaults", "docker_name", "label", "module_id", "module_sha256", "parameter_choices", "provides", "requires"]);
const digest = text(row.module_sha256);
if (!SHA256.test(digest)) throw new Error("Каталог вернул некорректную версию AI-модуля.");
return {
moduleId: text(row.module_id), moduleSha256: digest, label: text(row.label),
dockerName: text(row.docker_name), requires: array(row.requires).map(text),
provides: array(row.provides).map(text), defaults: record(row.defaults, "параметры AI-модуля"),
};
}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Некорректный ${label}.`);
return value as Record<string, unknown>;
}
function array(value: unknown): unknown[] { if (!Array.isArray(value)) throw new Error("Ожидался список."); return value; }
function text(value: unknown): string { if (typeof value !== "string" || !value) throw new Error("Ожидался текст."); return value; }
function boolean(value: unknown): boolean { if (typeof value !== "boolean") throw new Error("Ожидалось логическое значение."); return value; }
function exactKeys(value: Record<string, unknown>, expected: string[]): void {
if (Object.keys(value).sort().join() !== [...expected].sort().join()) throw new Error("Контракт AI-слоя изменился.");
}
async function bodyOf(response: Response): Promise<unknown> { const value = await response.text(); try { return JSON.parse(value); } catch { return value; } }
function apiError(body: unknown, status: number): Error {
const detail = body && typeof body === "object" && !Array.isArray(body) ? (body as Record<string, unknown>).detail : null;
return new Error(typeof detail === "string" ? detail : `Observatory API вернул HTTP ${status}.`);
}
function createIdempotencyKey(): string {
const entropy = typeof globalThis.crypto?.randomUUID === "function"
? globalThis.crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
return `ai-layer:${entropy}`;
}
@@ -0,0 +1,321 @@
import {
type ObservationLabInstance,
type ObservationSessionFetch,
type ObservationSessionSummary,
} from "../observation/sessionArchive";
import { fetchObservationSessionCatalogPage } from "../observation/sessionCatalogPage";
import {
observatoryRecordedRunBinding,
type ObservatoryRecordedRunBinding,
} from "./recordedRun";
export interface ObservatoryEvidence {
readonly sessionId: string;
readonly label: string;
readonly status: ObservationSessionSummary["status"];
readonly publishedAtUtc: string;
readonly lab: ObservationLabInstance;
readonly recordedRun: ObservatoryRecordedRunBinding | null;
}
export interface ObservatorySession {
readonly source: ObservationSessionSummary;
readonly evidence: readonly ObservatoryEvidence[];
}
export interface ObservatoryCatalog {
readonly items: readonly ObservatorySession[];
readonly unresolvedEvidence: readonly ObservatoryEvidence[];
readonly window: {
readonly limit: number;
readonly sourceCount: number;
readonly laboratoryCount: number;
readonly sourceLimitReached: boolean;
readonly laboratoryLimitReached: boolean;
};
}
export type ObservatoryCatalogMutation =
| {
readonly kind: "rename";
readonly displayName: string;
readonly revision: number;
}
| {
readonly kind: "delete";
readonly revision: number;
};
export type ObservatoryCatalogMutationOverlay = ReadonlyMap<
string,
ObservatoryCatalogMutation
>;
export interface ObservatoryCatalogReconciliation {
readonly catalog: ObservatoryCatalog;
readonly overlay: ObservatoryCatalogMutationOverlay;
}
export class ObservatoryCatalogContractError extends Error {
constructor(message: string) {
super(message);
this.name = "ObservatoryCatalogContractError";
}
}
export function findObservatoryEvidence(
catalog: ObservatoryCatalog,
sessionId: string,
): ObservatoryEvidence | null {
for (const item of catalog.items) {
const evidence = item.evidence.find(
(candidate) => candidate.sessionId === sessionId,
);
if (evidence) return evidence;
}
return catalog.unresolvedEvidence.find(
(candidate) => candidate.sessionId === sessionId,
) ?? null;
}
export function observatoryCatalogConfirmsEvidenceDeletion(
catalog: ObservatoryCatalog,
sessionId: string,
): boolean {
return !catalog.window.laboratoryLimitReached
&& findObservatoryEvidence(catalog, sessionId) === null;
}
export function applyObservatoryCatalogMutationOverlay(
catalog: ObservatoryCatalog,
overlay: ObservatoryCatalogMutationOverlay,
): ObservatoryCatalog {
if (overlay.size === 0) return catalog;
const removedSessionIds = new Set<string>();
const projectEvidence = (
evidence: ObservatoryEvidence,
): ObservatoryEvidence | null => {
const mutation = overlay.get(evidence.sessionId);
if (!mutation) return evidence;
if (mutation.kind === "delete") {
removedSessionIds.add(evidence.sessionId);
return null;
}
return evidence.label === mutation.displayName
? evidence
: { ...evidence, label: mutation.displayName };
};
const projectEvidenceList = (
evidence: readonly ObservatoryEvidence[],
): ObservatoryEvidence[] => evidence.flatMap((candidate) => {
const projected = projectEvidence(candidate);
return projected ? [projected] : [];
});
const items = catalog.items.map((item) => ({
...item,
evidence: projectEvidenceList(item.evidence),
}));
const unresolvedEvidence = projectEvidenceList(catalog.unresolvedEvidence);
return {
...catalog,
items,
unresolvedEvidence,
window: {
...catalog.window,
laboratoryCount: Math.max(
0,
catalog.window.laboratoryCount - removedSessionIds.size,
),
},
};
}
export function reconcileObservatoryCatalogMutationOverlay(
serverCatalog: ObservatoryCatalog,
overlay: ObservatoryCatalogMutationOverlay,
requestMutationRevision: number,
): ObservatoryCatalogReconciliation {
const remaining = new Map(overlay);
for (const [sessionId, mutation] of overlay) {
if (mutation.revision > requestMutationRevision) continue;
const evidence = findObservatoryEvidence(serverCatalog, sessionId);
const confirmed = mutation.kind === "rename"
? evidence?.label === mutation.displayName
: observatoryCatalogConfirmsEvidenceDeletion(serverCatalog, sessionId);
if (confirmed) remaining.delete(sessionId);
}
return {
catalog: applyObservatoryCatalogMutationOverlay(serverCatalog, remaining),
overlay: remaining,
};
}
function newestFirst(left: string, right: string): number {
return Date.parse(right) - Date.parse(left);
}
function boundedLimit(limit: number): number {
return Number.isFinite(limit)
? Math.min(100, Math.max(1, Math.floor(limit)))
: 100;
}
function evidenceFromSession(
session: ObservationSessionSummary,
): ObservatoryEvidence | null {
if (!session.lab) {
throw new ObservatoryCatalogContractError(
`LAB-каталог вернул сессию ${session.id} без типизированной связи с источником.`,
);
}
// Historical LABs retain their archive and replay adapters. Observatory is
// the portable-profile product; a familiar LAB label is not admission.
if (session.lab.replayCapability?.kind !== "portable-result-review") return null;
const recordedRun = observatoryRecordedRunBinding(session.id, session.lab);
return {
sessionId: session.id,
label: session.label,
status: session.status,
publishedAtUtc: session.lab.publishedAtUtc,
lab: session.lab,
recordedRun,
};
}
function sortEvidence(
evidence: readonly ObservatoryEvidence[],
): ObservatoryEvidence[] {
return [...evidence].sort((left, right) =>
newestFirst(left.publishedAtUtc, right.publishedAtUtc)
|| left.sessionId.localeCompare(right.sessionId));
}
export function buildObservatoryCatalog(
sourceSessions: readonly ObservationSessionSummary[],
laboratorySessions: readonly ObservationSessionSummary[],
limit = 100,
): ObservatoryCatalog {
const safeLimit = boundedLimit(limit);
const sources = new Map<string, ObservationSessionSummary>();
for (const source of sourceSessions) {
if (source.lab !== null) {
throw new ObservatoryCatalogContractError(
`Каталог источников вернул LAB-сессию ${source.id}.`,
);
}
sources.set(source.id, source);
}
const linked = new Map<string, ObservatoryEvidence[]>();
const unresolvedEvidence: ObservatoryEvidence[] = [];
for (const laboratorySession of laboratorySessions) {
const evidence = evidenceFromSession(laboratorySession);
if (!evidence) continue;
const sourceId = evidence.lab.sourceSessionId;
if (!sources.has(sourceId)) {
// A bounded traversal may stop before this source. Absence alone is
// not proof of a broken relationship or permission to remove data.
unresolvedEvidence.push(evidence);
continue;
}
const current = linked.get(sourceId) ?? [];
current.push(evidence);
linked.set(sourceId, current);
}
const items = [...sources.values()]
.sort((left, right) =>
newestFirst(left.startedAtUtc, right.startedAtUtc)
|| left.id.localeCompare(right.id))
.map((source) => ({
source,
evidence: sortEvidence(linked.get(source.id) ?? []),
}));
return {
items,
unresolvedEvidence: sortEvidence(unresolvedEvidence),
window: {
limit: safeLimit,
sourceCount: sourceSessions.length,
laboratoryCount: [...linked.values()].reduce((count, items) => count + items.length, 0)
+ unresolvedEvidence.length,
sourceLimitReached: sourceSessions.length >= safeLimit,
laboratoryLimitReached: laboratorySessions.length >= safeLimit,
},
};
}
export async function fetchObservatoryCatalog({
signal,
limit = 100,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
limit?: number;
fetcher?: ObservationSessionFetch;
} = {}): Promise<ObservatoryCatalog> {
const safeLimit = boundedLimit(limit);
const request = new AbortController();
const abort = () => request.abort(signal?.reason);
if (signal?.aborted) abort();
else signal?.addEventListener("abort", abort, { once: true });
try {
const [sources, laboratories] = await Promise.all([
readCatalogPages("source", safeLimit, fetcher, request.signal),
readCatalogPages("laboratory", safeLimit, fetcher, request.signal),
]);
const catalog = buildObservatoryCatalog(sources.items, laboratories.items, safeLimit);
return {
...catalog,
window: {
...catalog.window,
sourceLimitReached: sources.hasMore,
laboratoryLimitReached: laboratories.hasMore,
},
};
} finally {
request.abort();
signal?.removeEventListener("abort", abort);
}
}
// Metadata only, never recordings or result artifacts. The cap is an explicit
// partial window, not silent completeness or an unbounded background crawler.
const MAX_CATALOG_PAGES = 256;
async function readCatalogPages(
scope: "source" | "laboratory",
limit: number,
fetcher: ObservationSessionFetch,
signal: AbortSignal,
): Promise<{ items: ObservationSessionSummary[]; hasMore: boolean }> {
const items: ObservationSessionSummary[] = [];
const seenIds = new Set<string>();
const cursors = new Set<string>();
let cursor: string | null = null;
for (let index = 0; index < MAX_CATALOG_PAGES; index += 1) {
const page = await fetchObservationSessionCatalogPage({ scope, limit, cursor, signal, fetcher });
for (const item of page.items) {
if (seenIds.has(item.id)) {
throw new ObservatoryCatalogContractError("Каталог повторил сессию между страницами.");
}
seenIds.add(item.id);
// Do not accumulate legacy provenance while walking the shared catalog.
if (scope === "source" || item.lab?.replayCapability?.kind === "portable-result-review") {
items.push(item);
} else if (!item.lab) {
throw new ObservatoryCatalogContractError("LAB-каталог вернул исходную сессию.");
}
}
cursor = page.nextCursor;
if (cursor === null) return { items, hasMore: false };
if (cursors.has(cursor)) {
throw new ObservatoryCatalogContractError("Каталог повторил курсор страницы.");
}
cursors.add(cursor);
}
return { items, hasMore: true };
}
@@ -0,0 +1,210 @@
import type { ObservatoryRecordedRunBinding } from "./recordedRun";
const SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const RENAME_SCHEMA = "missioncore.observatory-lab-projection-rename/v1";
const PROJECTION_SCHEMA = "missioncore.observatory-lab-projection/v1";
export type ObservatoryCatalogMutationFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
export interface ObservatoryLabProjectionMutationResult {
readonly schemaVersion: typeof PROJECTION_SCHEMA;
readonly sessionId: string;
readonly displayName: string;
}
export class ObservatoryCatalogMutationError extends Error {
readonly status: number | null;
constructor(message: string, status: number | null = null) {
super(message);
this.name = "ObservatoryCatalogMutationError";
this.status = status;
}
}
export async function renameObservatoryLabProjection(
binding: ObservatoryRecordedRunBinding,
displayName: string,
{
signal,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
fetcher?: ObservatoryCatalogMutationFetch;
} = {},
): Promise<ObservatoryLabProjectionMutationResult> {
const sessionId = admittedProjectionId(binding);
const normalizedName = displayName.trim();
if (normalizedName.length < 1 || normalizedName.length > 160) {
throw new ObservatoryCatalogMutationError(
"Название лабораторного результата должно содержать от 1 до 160 символов.",
);
}
const response = await request(
fetcher,
`/api/v1/observatory/lab-projections/${encodeURIComponent(sessionId)}`,
{
method: "PATCH",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
schema_version: RENAME_SCHEMA,
display_name: normalizedName,
}),
signal,
},
"Не удалось переименовать лабораторный результат.",
);
const body = await responseBody(response);
if (!response.ok || response.status !== 200) {
throw apiError(
body,
`Переименование лабораторного результата вернуло HTTP ${response.status}.`,
response.status,
);
}
const decoded = decodeProjection(body);
if (decoded.sessionId !== sessionId || decoded.displayName !== normalizedName) {
throw new ObservatoryCatalogMutationError(
"Сервер не подтвердил точное переименование выбранного результата.",
);
}
return decoded;
}
export async function deleteObservatoryLabProjection(
binding: ObservatoryRecordedRunBinding,
{
signal,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
fetcher?: ObservatoryCatalogMutationFetch;
} = {},
): Promise<void> {
const sessionId = admittedProjectionId(binding);
const response = await request(
fetcher,
`/api/v1/observatory/lab-projections/${encodeURIComponent(sessionId)}`,
{
method: "DELETE",
headers: { Accept: "application/json" },
signal,
},
"Не удалось удалить лабораторный результат из Обсерватории.",
);
const body = await responseBody(response);
if (!response.ok || response.status !== 204) {
throw apiError(
body,
`Удаление лабораторного результата вернуло HTTP ${response.status}.`,
response.status,
);
}
if (body !== undefined) {
throw new ObservatoryCatalogMutationError(
"Сервер вернул данные после подтверждённого удаления лабораторного результата.",
);
}
}
function admittedProjectionId(binding: ObservatoryRecordedRunBinding): string {
const sessionId = binding.evidenceSessionId;
const admittedViewer = (
binding.kind === "canonical-recorded-rerun"
&& binding.viewerProfile === "recorded-session"
&& binding.timeline === "session_time"
) || (
binding.kind === "portable-result-review"
&& binding.viewerProfile === "portable-result"
&& binding.timeline === "result-defined"
);
if (
!admittedViewer
|| binding.activation !== "explicit"
|| binding.resultId !== sessionId
|| binding.sourceSessionId === sessionId
|| !SAFE_SESSION_ID.test(sessionId)
) {
throw new ObservatoryCatalogMutationError(
"Выбранный результат не допущен к изменению каталога Обсерватории.",
);
}
return sessionId;
}
async function request(
fetcher: ObservatoryCatalogMutationFetch,
input: string,
init: RequestInit,
fallback: string,
): Promise<Response> {
try {
return await fetcher(input, init);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
throw new ObservatoryCatalogMutationError(fallback);
}
}
async function responseBody(response: Response): Promise<unknown> {
const text = await response.text();
if (!text) return undefined;
try {
return JSON.parse(text) as unknown;
} catch {
return text;
}
}
function apiError(body: unknown, fallback: string, status: number): Error {
if (isRecord(body) && typeof body.detail === "string" && body.detail.trim()) {
return new ObservatoryCatalogMutationError(body.detail.trim(), status);
}
if (typeof body === "string" && body.trim()) {
return new ObservatoryCatalogMutationError(body.trim(), status);
}
return new ObservatoryCatalogMutationError(fallback, status);
}
function decodeProjection(value: unknown): ObservatoryLabProjectionMutationResult {
if (!isRecord(value)) {
throw new ObservatoryCatalogMutationError(
"Сервер вернул некорректное подтверждение лабораторного результата.",
);
}
const keys = Object.keys(value).sort();
const expected = ["display_name", "schema_version", "session_id"];
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
throw new ObservatoryCatalogMutationError(
"Подтверждение лабораторного результата содержит неизвестные поля.",
);
}
if (
value.schema_version !== PROJECTION_SCHEMA
|| typeof value.session_id !== "string"
|| !SAFE_SESSION_ID.test(value.session_id)
|| typeof value.display_name !== "string"
|| value.display_name.trim() !== value.display_name
|| value.display_name.length < 1
|| value.display_name.length > 160
) {
throw new ObservatoryCatalogMutationError(
"Сервер вернул неподдерживаемый контракт лабораторного результата.",
);
}
return {
schemaVersion: PROJECTION_SCHEMA,
sessionId: value.session_id,
displayName: value.display_name,
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -0,0 +1,104 @@
import type { SceneSettings } from "../../sceneSettings";
const PROFILE_SCHEMA = "missioncore.observatory-lab-view-profile/v1";
export async function fetchLabViewProfile(
resultId: string,
signal?: AbortSignal,
): Promise<SceneSettings | null> {
const response = await fetch(`/api/v1/observatory/lab-view-profiles/${encodeURIComponent(resultId)}`, {
headers: { Accept: "application/json" },
signal,
});
if (response.status === 404) return null;
const body: unknown = await response.json();
if (!response.ok) throw new Error("Профиль отображения LAB не загрузился.");
return decodeProfile(body, resultId);
}
export async function saveLabViewProfile(
resultId: string,
settings: SceneSettings,
signal?: AbortSignal,
): Promise<SceneSettings> {
const response = await fetch(`/api/v1/observatory/lab-view-profiles/${encodeURIComponent(resultId)}`, {
method: "PUT",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
schema_version: PROFILE_SCHEMA,
result_id: resultId,
scene_settings: {
point_size: settings.pointSize,
accumulation_seconds: settings.accumulationSeconds,
color_mode: settings.colorMode,
palette: settings.palette,
show_grid: settings.showGrid,
show_labels: settings.showLabels,
show_camera_frustums: settings.showCameraFrustums,
},
}),
signal,
});
const body: unknown = await response.json();
if (!response.ok) throw new Error("Настройки LAB не сохранились. Повторите закрытие окна.");
return decodeProfile(body, resultId);
}
function decodeProfile(value: unknown, expectedResultId: string): SceneSettings {
const row = record(value);
exactKeys(row, ["result_id", "scene_settings", "schema_version", "updated_at_utc"]);
if (row.schema_version !== PROFILE_SCHEMA || row.result_id !== expectedResultId) {
throw new Error("Сервер вернул профиль другой LAB.");
}
const settings = record(row.scene_settings);
exactKeys(settings, [
"accumulation_seconds", "color_mode", "palette", "point_size",
"show_camera_frustums", "show_grid", "show_labels",
]);
const pointSize = finite(settings.point_size);
const accumulationSeconds = finite(settings.accumulation_seconds);
const colorMode = settings.color_mode;
const palette = settings.palette;
if (
pointSize < 0.1
|| accumulationSeconds < 0
|| !new Set(["intensity", "height", "distance", "rgb", "class"]).has(String(colorMode))
|| !new Set(["turbo", "viridis", "plasma", "grayscale"]).has(String(palette))
|| typeof settings.show_grid !== "boolean"
|| typeof settings.show_labels !== "boolean"
|| typeof settings.show_camera_frustums !== "boolean"
) throw new Error("Сервер вернул повреждённый профиль LAB.");
return {
projection: "3d",
pointSize,
colorMode: colorMode as SceneSettings["colorMode"],
palette: palette as SceneSettings["palette"],
customColor: "#35d7c1",
accumulationSeconds,
showPoints: true,
showTrajectory: true,
showGrid: settings.show_grid,
showLabels: settings.show_labels,
showCameraFrustums: settings.show_camera_frustums,
};
}
function record(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("Некорректный профиль отображения LAB.");
}
return value as Record<string, unknown>;
}
function finite(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error("Некорректное числовое значение профиля LAB.");
}
return value;
}
function exactKeys(value: Record<string, unknown>, expected: string[]): void {
if (Object.keys(value).sort().join() !== [...expected].sort().join()) {
throw new Error("Контракт профиля отображения LAB изменился.");
}
}
@@ -0,0 +1,449 @@
export { fetchObservatoryPortableLaboratorySetups } from "./portableLaboratorySetups";
const CATALOG_SCHEMA = "missioncore.observatory-laboratory-setup-catalog/v1";
const PREFLIGHT_REQUEST_SCHEMA = "missioncore.observatory-run-preflight-request/v1";
const PREFLIGHT_SCHEMA = "missioncore.observatory-run-preflight/v1";
const SHA256 = /^[a-f0-9]{64}$/;
export type ObservatoryLaboratorySetupOrigin =
| "archived-definition"
| "existing-result"
| "portable-definition";
export type ObservatoryLaboratorySetupAction =
| "open-existing"
| "open-legacy"
| "check"
| "blocked";
export interface ObservatoryLaboratoryRunDefinition {
readonly definitionId: string;
readonly version: number;
readonly workId: string | null;
readonly definitionSha256: string;
readonly resultSchema: string | null;
readonly resultKind: string | null;
readonly models: readonly {
readonly name: string;
readonly releaseId: string;
readonly modelId: string;
readonly architecture: string;
}[];
readonly configuration: readonly {
readonly role: string;
readonly sha256: string;
}[];
}
export interface ObservatoryLaboratoryPreservedResult {
readonly resultId: string;
readonly resultKind: string;
readonly relation: string;
readonly access: "legacy-lab" | "observatory" | "evidence-only";
readonly createdAtUtc: string;
readonly observatoryProjectionAvailable: boolean;
}
export interface ObservatoryLaboratorySetup {
readonly setupId: string;
readonly displayName: string;
readonly description: string;
readonly origin: ObservatoryLaboratorySetupOrigin;
readonly runDefinition: ObservatoryLaboratoryRunDefinition | null;
readonly compatibility: {
readonly compatible: boolean;
readonly reasons: readonly { readonly code: string; readonly message: string }[];
};
readonly executor: {
readonly contourId: string;
readonly state: "not-installed" | "ready";
readonly reasonCode: string;
readonly reason: string;
};
readonly preservedResults: readonly ObservatoryLaboratoryPreservedResult[];
readonly preflight: {
readonly outcome: "existing" | "ready" | "blocked";
readonly action: ObservatoryLaboratorySetupAction;
readonly reason: string;
readonly submissionAllowed: boolean;
readonly existingResultIds: readonly string[];
};
}
export interface ObservatoryLaboratorySetupCatalog {
readonly sourceSessionId: string;
readonly setups: readonly ObservatoryLaboratorySetup[];
}
// Cache identity is verified by the portable catalog decoder/server, never by a LAB label.
export function selectableObservatorySetups(catalog: ObservatoryLaboratorySetupCatalog | null) {
return catalog?.setups.filter((setup) => (
setup.origin === "portable-definition"
&& setup.runDefinition !== null
&& setup.compatibility.compatible
&& setup.preflight.outcome !== "existing"
)) ?? [];
}
export interface ObservatoryLaboratoryRunPreflight {
readonly sourceSessionId: string;
readonly setupId: string;
readonly definitionSha256: string | null;
readonly checkSha256: string | null;
readonly outcome: "existing" | "queueable" | "blocked";
readonly submissionAllowed: boolean;
readonly checks: readonly {
readonly checkId: string;
readonly outcome: "pass" | "fail" | "not-applicable";
readonly reasonCode: string;
readonly message: string;
}[];
readonly existingResultIds: readonly string[];
}
export type ObservatoryLaboratorySetupFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
export class ObservatoryLaboratorySetupContractError extends Error {
readonly status: number | null;
constructor(message: string, status: number | null = null) {
super(message);
this.name = "ObservatoryLaboratorySetupContractError";
this.status = status;
}
}
export async function fetchObservatoryLaboratorySetups(
sourceSessionId: string,
{
signal,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
fetcher?: ObservatoryLaboratorySetupFetch;
} = {},
): Promise<ObservatoryLaboratorySetupCatalog> {
const response = await request(
fetcher,
`/api/v1/observatory/laboratory-setups?source_session_id=${encodeURIComponent(sourceSessionId)}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
const body = await responseBody(response);
if (!response.ok) throw apiError(body, response.status);
const catalog = decodeCatalog(body);
if (catalog.sourceSessionId !== sourceSessionId) {
throw new ObservatoryLaboratorySetupContractError(
"Каталог сетапов относится к другой исходной сессии.",
);
}
return catalog;
}
export async function preflightObservatoryLaboratorySetup(
sourceSessionId: string,
setup: ObservatoryLaboratorySetup,
{
signal,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
fetcher?: ObservatoryLaboratorySetupFetch;
} = {},
): Promise<ObservatoryLaboratoryRunPreflight> {
const response = await request(
fetcher,
"/api/v1/observatory/run-preflights",
{
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
schema_version: PREFLIGHT_REQUEST_SCHEMA,
source_session_id: sourceSessionId,
setup_id: setup.setupId,
definition_sha256: setup.runDefinition?.definitionSha256 ?? null,
}),
signal,
},
);
const body = await responseBody(response);
if (!response.ok) throw apiError(body, response.status);
const preflight = decodePreflight(body);
if (preflight.sourceSessionId !== sourceSessionId || preflight.setupId !== setup.setupId) {
throw new ObservatoryLaboratorySetupContractError(
"Preflight относится к другому источнику или сетапу.",
);
}
if (preflight.definitionSha256 !== (setup.runDefinition?.definitionSha256 ?? null)) {
throw new ObservatoryLaboratorySetupContractError(
"Preflight относится к другой версии сетапа.",
);
}
return preflight;
}
function decodeCatalog(value: unknown): ObservatoryLaboratorySetupCatalog {
const row = record(value, "каталог сетапов");
exactKeys(row, ["authority", "schema_version", "setups", "source_session_id"], "каталог сетапов");
exact(row.schema_version, CATALOG_SCHEMA, "schema_version каталога сетапов");
observationAuthority(row.authority);
return {
sourceSessionId: text(row.source_session_id, "source_session_id"),
setups: array(row.setups, "setups").map(decodeSetup),
};
}
function decodeSetup(value: unknown): ObservatoryLaboratorySetup {
const row = record(value, "сетап");
exactKeys(row, [
"authority", "compatibility", "description", "display_name", "executor", "origin",
"preflight", "preserved_results", "run_definition", "setup_id", "source",
], "сетап");
observationAuthority(row.authority);
const origin = oneOf(row.origin, ["archived-definition", "existing-result"] as const, "origin");
const compatibility = record(row.compatibility, "compatibility");
const executor = record(row.executor, "executor");
const preflight = record(row.preflight, "preflight");
const source = record(row.source, "source");
const runDefinition = row.run_definition === null
? null
: decodeRunDefinition(row.run_definition);
if ((origin === "archived-definition") !== (runDefinition !== null)) {
throw new ObservatoryLaboratorySetupContractError(
"Происхождение сетапа не совпадает с RunDefinition.",
);
}
exactKeys(source, ["label", "required_modalities", "session_id"], "source");
text(source.session_id, "source.session_id");
text(source.label, "source.label");
array(source.required_modalities, "source.required_modalities").forEach((item) => text(item, "source modality"));
exactKeys(compatibility, ["compatible", "reasons"], "compatibility");
exactKeys(executor, ["contour_id", "reason", "reason_code", "state"], "executor");
exactKeys(preflight, ["action", "existing_result_ids", "outcome", "reason", "submission_allowed"], "preflight");
return {
setupId: text(row.setup_id, "setup_id"),
displayName: text(row.display_name, "display_name"),
description: text(row.description, "description"),
origin,
runDefinition,
compatibility: {
compatible: boolean(row.compatibility && compatibility.compatible, "compatible"),
reasons: array(compatibility.reasons, "compatibility.reasons").map((item) => {
const reason = record(item, "compatibility reason");
exactKeys(reason, ["code", "message"], "compatibility reason");
return { code: text(reason.code, "reason.code"), message: text(reason.message, "reason.message") };
}),
},
executor: {
contourId: text(executor.contour_id, "executor.contour_id"),
state: exact(executor.state, "not-installed", "executor.state"),
reasonCode: text(executor.reason_code, "executor.reason_code"),
reason: text(executor.reason, "executor.reason"),
},
preservedResults: array(row.preserved_results, "preserved_results").map(decodePreservedResult),
preflight: {
outcome: oneOf(preflight.outcome, ["existing", "blocked"] as const, "preflight.outcome"),
action: oneOf(preflight.action, ["open-existing", "open-legacy", "blocked"] as const, "preflight.action"),
reason: text(preflight.reason, "preflight.reason"),
submissionAllowed: exact(preflight.submission_allowed, false, "preflight.submission_allowed"),
existingResultIds: array(preflight.existing_result_ids, "preflight.existing_result_ids").map((item) => text(item, "existing result id")),
},
};
}
function decodeRunDefinition(value: unknown): ObservatoryLaboratoryRunDefinition {
const row = record(value, "RunDefinition");
exactKeys(row, [
"authority", "configuration", "definition_id", "definition_sha256", "schema_version",
"source", "version", "work_id",
], "RunDefinition");
exact(row.schema_version, "missioncore.observatory-run-definition/v1", "RunDefinition schema");
observationAuthority(row.authority);
const source = record(row.source, "RunDefinition source");
exactKeys(source, ["label", "required_modalities", "session_id"], "RunDefinition source");
text(source.session_id, "RunDefinition source.session_id");
text(source.label, "RunDefinition source.label");
array(source.required_modalities, "RunDefinition source.required_modalities")
.forEach((item) => text(item, "RunDefinition source modality"));
const digest = text(row.definition_sha256, "definition_sha256");
if (!SHA256.test(digest)) throw new ObservatoryLaboratorySetupContractError("Некорректный digest RunDefinition.");
return {
definitionId: text(row.definition_id, "definition_id"),
version: positiveInteger(row.version, "definition version"),
workId: text(row.work_id, "work_id"),
definitionSha256: digest,
resultSchema: null,
resultKind: null,
models: [],
configuration: array(row.configuration, "configuration").map((item) => {
const reference = record(item, "configuration reference");
exactKeys(reference, ["role", "sha256"], "configuration reference");
const sha256 = text(reference.sha256, "configuration sha256");
if (!SHA256.test(sha256)) throw new ObservatoryLaboratorySetupContractError("Некорректный digest конфигурации.");
return { role: text(reference.role, "configuration role"), sha256 };
}),
};
}
function decodePreservedResult(value: unknown): ObservatoryLaboratoryPreservedResult {
const row = record(value, "preserved result");
exactKeys(row, [
"access", "created_at_utc", "observatory_projection_available", "relation",
"result_id", "result_kind",
], "preserved result");
return {
resultId: text(row.result_id, "result_id"),
resultKind: text(row.result_kind, "result_kind"),
relation: text(row.relation, "relation"),
access: oneOf(row.access, ["legacy-lab", "observatory", "evidence-only"] as const, "result access"),
createdAtUtc: text(row.created_at_utc, "created_at_utc"),
observatoryProjectionAvailable: boolean(row.observatory_projection_available, "observatory projection availability"),
};
}
function decodePreflight(value: unknown): ObservatoryLaboratoryRunPreflight {
const row = record(value, "preflight");
const baseKeys = [
"authority", "checks", "definition_sha256", "executor", "existing_result_ids",
"outcome", "schema_version", "setup_id", "source_session_id", "submission_allowed",
] as const;
const hasPortableCheck = Object.hasOwn(row, "check_sha256");
exactKeys(
row,
hasPortableCheck ? [...baseKeys, "check_sha256"] : baseKeys,
"preflight",
);
exact(row.schema_version, PREFLIGHT_SCHEMA, "preflight schema");
observationAuthority(row.authority);
const digest = row.definition_sha256;
if (digest !== null && (typeof digest !== "string" || !SHA256.test(digest))) {
throw new ObservatoryLaboratorySetupContractError("Некорректный digest preflight.");
}
const checkDigest = hasPortableCheck ? row.check_sha256 : null;
if (
checkDigest !== null
&& (typeof checkDigest !== "string" || !SHA256.test(checkDigest))
) {
throw new ObservatoryLaboratorySetupContractError(
"Некорректный check digest preflight.",
);
}
const outcome = oneOf(
row.outcome,
["existing", "queueable", "blocked"] as const,
"preflight outcome",
);
const submissionAllowed = boolean(
row.submission_allowed,
"preflight submission_allowed",
);
if (
submissionAllowed !== (outcome === "queueable")
|| (hasPortableCheck && outcome === "queueable" && checkDigest === null)
) {
throw new ObservatoryLaboratorySetupContractError(
"Preflight содержит противоречивое разрешение постановки в очередь.",
);
}
return {
sourceSessionId: text(row.source_session_id, "preflight source_session_id"),
setupId: text(row.setup_id, "preflight setup_id"),
definitionSha256: digest,
checkSha256: checkDigest,
outcome,
submissionAllowed,
checks: array(row.checks, "preflight checks").map((item) => {
const check = record(item, "preflight check");
exactKeys(check, ["check_id", "message", "outcome", "reason_code"], "preflight check");
return {
checkId: text(check.check_id, "check_id"),
outcome: oneOf(check.outcome, ["pass", "fail", "not-applicable"] as const, "check outcome"),
reasonCode: text(check.reason_code, "check reason_code"),
message: text(check.message, "check message"),
};
}),
existingResultIds: array(row.existing_result_ids, "existing_result_ids").map((item) => text(item, "existing result id")),
};
}
function observationAuthority(value: unknown): void {
const row = record(value, "authority");
const keys = ["commands_enabled", "actuation_allowed", "navigation_or_safety_accepted", "production_accepted"];
exactKeys(row, keys, "authority");
for (const key of keys) {
exact(row[key], false, `authority.${key}`);
}
}
async function request(fetcher: ObservatoryLaboratorySetupFetch, input: string, init: RequestInit): Promise<Response> {
try {
return await fetcher(input, init);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
throw new ObservatoryLaboratorySetupContractError("Каталог сетапов недоступен.");
}
}
async function responseBody(response: Response): Promise<unknown> {
const textBody = await response.text();
if (!textBody) return undefined;
try { return JSON.parse(textBody) as unknown; } catch { return textBody; }
}
function apiError(body: unknown, status: number): ObservatoryLaboratorySetupContractError {
const detail = body && typeof body === "object" && !Array.isArray(body)
? (body as Record<string, unknown>).detail
: null;
return new ObservatoryLaboratorySetupContractError(
typeof detail === "string" && detail.trim() ? detail : `Observatory API вернул HTTP ${status}.`,
status,
);
}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new ObservatoryLaboratorySetupContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function array(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) throw new ObservatoryLaboratorySetupContractError(`${label}: ожидался массив.`);
return value;
}
function text(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) throw new ObservatoryLaboratorySetupContractError(`${label}: ожидался текст.`);
return value;
}
function boolean(value: unknown, label: string): boolean {
if (typeof value !== "boolean") throw new ObservatoryLaboratorySetupContractError(`${label}: ожидался boolean.`);
return value;
}
function positiveInteger(value: unknown, label: string): number {
if (!Number.isInteger(value) || Number(value) < 1) throw new ObservatoryLaboratorySetupContractError(`${label}: ожидалось положительное целое.`);
return Number(value);
}
function exact<T>(value: unknown, expected: T, label: string): T {
if (value !== expected) throw new ObservatoryLaboratorySetupContractError(`${label}: значение изменилось.`);
return expected;
}
function oneOf<const T extends readonly string[]>(value: unknown, allowed: T, label: string): T[number] {
if (typeof value !== "string" || !allowed.includes(value)) {
throw new ObservatoryLaboratorySetupContractError(`${label}: значение не поддерживается.`);
}
return value as T[number];
}
function exactKeys(value: Record<string, unknown>, expected: readonly string[], label: string): void {
const actual = Object.keys(value).sort();
const sortedExpected = [...expected].sort();
if (actual.length !== sortedExpected.length || actual.some((key, index) => key !== sortedExpected[index])) {
throw new ObservatoryLaboratorySetupContractError(`${label}: обнаружены неизвестные поля.`);
}
}
@@ -0,0 +1,373 @@
import type {
ObservatoryLaboratoryPreservedResult,
ObservatoryLaboratoryRunDefinition,
ObservatoryLaboratorySetup,
ObservatoryLaboratorySetupCatalog,
} from "./laboratorySetups";
const PORTABLE_CATALOG_SCHEMA = "missioncore.observatory-portable-setup-catalog/v2";
const SHA256 = /^[a-f0-9]{64}$/;
export class ObservatoryPortableSetupDecodeError extends Error {
constructor(message: string) {
super(message);
this.name = "ObservatoryPortableSetupDecodeError";
}
}
export function decodePortableCatalog(value: unknown): ObservatoryLaboratorySetupCatalog {
const row = record(value, "portable-каталог сетапов");
exactKeys(
row,
["authority", "schema_version", "setups", "source_session_id"],
"portable-каталог сетапов",
);
exact(row.schema_version, PORTABLE_CATALOG_SCHEMA, "schema_version portable-каталога");
observationAuthority(row.authority);
const sourceSessionId = text(row.source_session_id, "portable source_session_id");
return {
sourceSessionId,
setups: array(row.setups, "portable setups").map((item) => decodePortableSetup(item, sourceSessionId)),
};
}
function decodePortableSetup(value: unknown, sourceSessionId: string): ObservatoryLaboratorySetup {
const row = record(value, "portable-сетап");
exactKeys(row, [
"authority", "description", "display_name", "executor", "existing_results",
"origin", "preflight", "run_definition", "setup_id", "source_compatibility",
"source_requirements",
], "portable-сетап");
exact(row.origin, "portable-definition", "portable origin");
observationAuthority(row.authority);
decodePortableSourceRequirements(row.source_requirements);
const setupId = text(row.setup_id, "portable setup_id");
const runDefinition = decodePortableRunDefinition(row.run_definition);
const compatibility = record(row.source_compatibility, "portable source_compatibility");
exactKeys(
compatibility,
["compatible", "outcome", "reason", "reason_code"],
"portable source_compatibility",
);
const compatible = boolean(compatibility.compatible, "portable compatible");
exact(
compatibility.outcome,
compatible ? "pass" : "blocked",
"portable compatibility outcome",
);
const compatibilityReason = text(compatibility.reason, "portable compatibility reason");
const compatibilityReasonCode = text(
compatibility.reason_code,
"portable compatibility reason_code",
);
const executor = record(row.executor, "portable executor");
exactKeys(
executor,
["contour_id", "ready", "reason", "reason_code", "state"],
"portable executor",
);
const executorState = oneOf(
executor.state,
["not-installed", "ready"] as const,
"portable executor state",
);
const executorReady = boolean(executor.ready, "portable executor ready");
if (executorReady !== (executorState === "ready")) {
throw new ObservatoryPortableSetupDecodeError(
"Portable executor: состояние готовности противоречиво.",
);
}
const executorReason = executor.reason === null
? "Исполнитель установлен."
: text(executor.reason, "portable executor reason");
const executorReasonCode = executor.reason_code === null
? "portable-executor-ready"
: text(executor.reason_code, "portable executor reason_code");
if (
(executorReady && (executor.reason !== null || executor.reason_code !== null))
|| (!executorReady && (executor.reason === null || executor.reason_code === null))
) {
throw new ObservatoryPortableSetupDecodeError(
"Portable executor: причина недоступности противоречит состоянию.",
);
}
const preflight = record(row.preflight, "portable preflight");
exactKeys(preflight, [
"action", "existing_result_ids", "outcome", "reason", "submission_allowed",
], "portable preflight");
const preflightOutcome = oneOf(
preflight.outcome,
["existing", "ready", "blocked"] as const,
"portable preflight outcome",
);
const preflightAction = oneOf(
preflight.action,
["open-existing", "check", "blocked"] as const,
"portable preflight action",
);
const submissionAllowed = boolean(
preflight.submission_allowed,
"portable preflight submission_allowed",
);
if (
submissionAllowed !== (preflightOutcome === "ready")
|| (preflightOutcome === "ready" && preflightAction !== "check")
|| (preflightOutcome === "blocked" && preflightAction !== "blocked")
|| (preflightOutcome === "existing" && preflightAction !== "open-existing")
|| (submissionAllowed && (!compatible || !executorReady))
) {
throw new ObservatoryPortableSetupDecodeError(
"Portable preflight: состояние запуска противоречиво.",
);
}
const existingResults = array(row.existing_results, "portable existing_results").map(
(item) => decodePortableResult(item, sourceSessionId, setupId, runDefinition),
);
const existingResultIds = array(
preflight.existing_result_ids,
"portable existing_result_ids",
).map((item) => text(item, "portable existing result id"));
if (
(existingResults.length > 0) !== (preflightOutcome === "existing")
|| existingResultIds.length !== existingResults.length
|| new Set(existingResultIds).size !== existingResultIds.length
|| existingResults.some((result, index) => result.resultId !== existingResultIds[index])
) {
throw new ObservatoryPortableSetupDecodeError(
"Portable result: готовность не соответствует проверенным результатам.",
);
}
return {
setupId,
displayName: text(row.display_name, "portable display_name"),
description: text(row.description, "portable description"),
origin: "portable-definition",
runDefinition,
compatibility: {
compatible,
reasons: compatible
? []
: [{ code: compatibilityReasonCode, message: compatibilityReason }],
},
executor: {
contourId: text(executor.contour_id, "portable executor contour_id"),
state: executorState,
reasonCode: executorReasonCode,
reason: executorReason,
},
preservedResults: existingResults,
preflight: {
outcome: preflightOutcome,
action: preflightAction,
reason: text(preflight.reason, "portable preflight reason"),
submissionAllowed,
existingResultIds,
},
};
}
function decodePortableResult(
value: unknown, sourceSessionId: string, setupId: string,
definition: ObservatoryLaboratoryRunDefinition,
): ObservatoryLaboratoryPreservedResult {
const row = record(value, "portable result");
exactKeys(row, [
"result_id", "result_kind", "relation", "access", "created_at_utc",
"observatory_projection_available", "identity",
], "portable result");
exact(row.result_kind, definition.resultKind, "portable result kind");
exact(row.relation, "exact-recorded-computation", "portable result relation");
exact(row.access, "observatory", "portable result access");
exact(row.observatory_projection_available, true, "portable result availability");
const identity = record(row.identity, "portable result identity");
exactKeys(identity, [
"job_id", "source_session_id", "source_catalog_sha256", "source_bundle_sha256",
"source_capability_manifest_sha256", "setup_id", "definition_sha256",
"package_sha256", "artifact_manifest_id",
], "portable result identity");
exact(identity.source_session_id, sourceSessionId, "portable result source");
exact(identity.setup_id, setupId, "portable result setup");
exact(identity.definition_sha256, definition.definitionSha256, "portable result definition");
text(identity.job_id, "portable result job");
for (const key of [
"source_catalog_sha256", "source_bundle_sha256", "source_capability_manifest_sha256",
"definition_sha256", "package_sha256", "artifact_manifest_id",
]) {
if (!SHA256.test(text(identity[key], `portable result ${key}`))) {
throw new ObservatoryPortableSetupDecodeError(`Portable result: некорректный ${key}.`);
}
}
return {
resultId: text(row.result_id, "portable result id"),
resultKind: text(row.result_kind, "portable result kind"),
relation: "exact-recorded-computation",
access: "observatory",
createdAtUtc: text(row.created_at_utc, "portable result created_at_utc"),
observatoryProjectionAvailable: true,
};
}
function decodePortableSourceRequirements(value: unknown): void {
const row = record(value, "portable source_requirements");
exactKeys(row, [
"archive_id", "calibration_identity_sha256", "calibration_slot", "camera_height",
"camera_semantic_channel_id", "camera_source_id", "camera_width",
"exactly_one_media_epoch", "plugin_id", "recorded_media_init_sha256",
"recorded_media_type", "required_modalities", "seekable",
], "portable source_requirements");
for (const key of [
"archive_id", "calibration_slot", "camera_semantic_channel_id", "camera_source_id",
"plugin_id", "recorded_media_type",
]) text(row[key], `portable source_requirements.${key}`);
for (const key of ["calibration_identity_sha256", "recorded_media_init_sha256"]) {
const digest = text(row[key], `portable source_requirements.${key}`);
if (!SHA256.test(digest)) {
throw new ObservatoryPortableSetupDecodeError(
`portable source_requirements.${key}: некорректный digest.`,
);
}
}
positiveInteger(row.camera_width, "portable camera_width");
positiveInteger(row.camera_height, "portable camera_height");
exact(row.exactly_one_media_epoch, true, "portable exactly_one_media_epoch");
exact(row.seekable, true, "portable seekable");
array(row.required_modalities, "portable required_modalities")
.forEach((item) => text(item, "portable required modality"));
}
function decodePortableRunDefinition(value: unknown): ObservatoryLaboratoryRunDefinition {
const row = record(value, "portable RunDefinition");
exactKeys(row, [
"definition_id", "definition_sha256", "models", "result_kind", "result_schema",
"version",
], "portable RunDefinition");
const digest = text(row.definition_sha256, "portable definition_sha256");
if (!SHA256.test(digest)) {
throw new ObservatoryPortableSetupDecodeError(
"Portable RunDefinition: некорректный digest.",
);
}
return {
definitionId: text(row.definition_id, "portable definition_id"),
version: positiveInteger(row.version, "portable definition version"),
workId: null,
definitionSha256: digest,
resultSchema: text(row.result_schema, "portable result_schema"),
resultKind: text(row.result_kind, "portable result_kind"),
models: array(row.models, "portable models").map((item) => {
const model = record(item, "portable model");
exactKeys(
model,
["architecture", "model_id", "name", "release_id"],
"portable model",
);
return {
name: text(model.name, "portable model name"),
releaseId: text(model.release_id, "portable model release_id"),
modelId: text(model.model_id, "portable model model_id"),
architecture: text(model.architecture, "portable model architecture"),
};
}),
configuration: [],
};
}
function observationAuthority(value: unknown): void {
const row = record(value, "authority");
const keys = [
"commands_enabled",
"actuation_allowed",
"navigation_or_safety_accepted",
"production_accepted",
];
exactKeys(row, keys, "authority");
for (const key of keys) exact(row[key], false, `authority.${key}`);
}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new ObservatoryPortableSetupDecodeError(
`${label}: ожидался объект.`,
);
}
return value as Record<string, unknown>;
}
function array(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) {
throw new ObservatoryPortableSetupDecodeError(
`${label}: ожидался массив.`,
);
}
return value;
}
function text(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new ObservatoryPortableSetupDecodeError(
`${label}: ожидался текст.`,
);
}
return value;
}
function boolean(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new ObservatoryPortableSetupDecodeError(
`${label}: ожидался boolean.`,
);
}
return value;
}
function positiveInteger(value: unknown, label: string): number {
if (!Number.isInteger(value) || Number(value) < 1) {
throw new ObservatoryPortableSetupDecodeError(
`${label}: ожидалось положительное целое.`,
);
}
return Number(value);
}
function exact<T>(value: unknown, expected: T, label: string): T {
if (value !== expected) {
throw new ObservatoryPortableSetupDecodeError(
`${label}: значение изменилось.`,
);
}
return expected;
}
function oneOf<const T extends readonly string[]>(
value: unknown,
allowed: T,
label: string,
): T[number] {
if (typeof value !== "string" || !allowed.includes(value)) {
throw new ObservatoryPortableSetupDecodeError(
`${label}: значение не поддерживается.`,
);
}
return value as T[number];
}
function exactKeys(
value: Record<string, unknown>,
expected: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const sortedExpected = [...expected].sort();
if (
actual.length !== sortedExpected.length
|| actual.some((key, index) => key !== sortedExpected[index])
) {
throw new ObservatoryPortableSetupDecodeError(
`${label}: обнаружены неизвестные поля.`,
);
}
}
@@ -0,0 +1,85 @@
import { decodePortableCatalog } from "./portableLaboratorySetupDecoder";
import type {
ObservatoryLaboratorySetupCatalog,
} from "./laboratorySetups";
export type ObservatoryPortableLaboratorySetupFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
class ObservatoryPortableLaboratorySetupContractError extends Error {
readonly status: number | null;
constructor(message: string, status: number | null = null) {
super(message);
this.name = "ObservatoryPortableLaboratorySetupContractError";
this.status = status;
}
}
export async function fetchObservatoryPortableLaboratorySetups(
sourceSessionId: string,
{
signal,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
fetcher?: ObservatoryPortableLaboratorySetupFetch;
} = {},
): Promise<ObservatoryLaboratorySetupCatalog> {
const response = await request(
fetcher,
`/api/v1/observatory/portable-laboratory-setups?source_session_id=${encodeURIComponent(sourceSessionId)}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
const body = await responseBody(response);
if (!response.ok) throw apiError(body, response.status);
const catalog = decodePortableCatalog(body);
if (catalog.sourceSessionId !== sourceSessionId) {
throw new ObservatoryPortableLaboratorySetupContractError(
"Portable-каталог сетапов относится к другой исходной сессии.",
);
}
return catalog;
}
async function request(
fetcher: ObservatoryPortableLaboratorySetupFetch,
input: string,
init: RequestInit,
): Promise<Response> {
try {
return await fetcher(input, init);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
throw new ObservatoryPortableLaboratorySetupContractError(
"Portable-каталог сетапов недоступен.",
);
}
}
async function responseBody(response: Response): Promise<unknown> {
const textBody = await response.text();
if (!textBody) return undefined;
try {
return JSON.parse(textBody) as unknown;
} catch {
return textBody;
}
}
function apiError(
body: unknown,
status: number,
): ObservatoryPortableLaboratorySetupContractError {
const detail = body && typeof body === "object" && !Array.isArray(body)
? (body as Record<string, unknown>).detail
: null;
return new ObservatoryPortableLaboratorySetupContractError(
typeof detail === "string" && detail.trim()
? detail
: `Observatory API вернул HTTP ${status}.`,
status,
);
}
@@ -0,0 +1,338 @@
const SUBMIT_SCHEMA = "missioncore.observatory-recorded-run-submit/v1";
const JOB_SCHEMA = "missioncore.observatory-recorded-job/v1";
const JOB_LIST_SCHEMA = "missioncore.observatory-recorded-job-list/v1";
const SHA256 = /^[a-f0-9]{64}$/;
export type ObservatoryRecordedJobState =
| "accepted"
| "queued"
| "claimed"
| "running"
| "paused"
| "preemption-pending"
| "succeeded"
| "failed"
| "reconciliation-required";
export type ObservatoryRecordedJobPublicationState =
| "not-required"
| "pending"
| "failed"
| "published";
export interface ObservatoryRecordedJob {
readonly jobId: string;
readonly idempotencyKey: string;
readonly sourceSessionId: string;
readonly setupId: string;
readonly definitionSha256: string;
readonly claimGeneration: number;
readonly state: ObservatoryRecordedJobState;
readonly restartFromZero: boolean;
readonly resultId: string | null;
readonly terminalMessage: string | null;
readonly publication: {
readonly state: ObservatoryRecordedJobPublicationState;
readonly attempts: number;
readonly error: string | null;
readonly publishedAtUtc: string | null;
};
readonly createdAtUtc: string;
readonly updatedAtUtc: string;
}
export type ObservatoryRecordedJobFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
export class ObservatoryRecordedJobContractError extends Error {
readonly status: number | null;
constructor(message: string, status: number | null = null) {
super(message);
this.name = "ObservatoryRecordedJobContractError";
this.status = status;
}
}
export async function fetchObservatoryRecordedJobs(
sourceSessionId: string,
setupId: string,
{
signal,
definitionSha256,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
definitionSha256?: string;
fetcher?: ObservatoryRecordedJobFetch;
} = {},
): Promise<readonly ObservatoryRecordedJob[]> {
const query = new URLSearchParams({
source_session_id: sourceSessionId,
setup_id: setupId,
limit: "20",
});
if (definitionSha256 !== undefined) query.set("definition_sha256", definitionSha256);
const response = await request(fetcher, `/api/v1/observatory/runs?${query}`, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
const body = await responseBody(response);
if (!response.ok) throw apiError(body, response.status);
const row = record(body, "список расчётов");
exactKeys(row, ["authority", "items", "schema_version"], "список расчётов");
exact(row.schema_version, JOB_LIST_SCHEMA, "schema_version списка расчётов");
observationAuthority(row.authority);
return array(row.items, "items").map(decodeObservatoryRecordedJob).filter((job) => (
job.sourceSessionId === sourceSessionId && job.setupId === setupId
&& (definitionSha256 === undefined || job.definitionSha256 === definitionSha256)
));
}
export async function submitObservatoryRecordedJob(
sourceSessionId: string,
setupId: string,
idempotencyKey: string,
portableBinding: {
readonly definitionSha256: string;
readonly checkSha256: string;
} | null,
{
signal,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
fetcher?: ObservatoryRecordedJobFetch;
} = {},
): Promise<ObservatoryRecordedJob> {
const response = await request(fetcher, "/api/v1/observatory/runs", {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
schema_version: SUBMIT_SCHEMA,
idempotency_key: idempotencyKey,
source_session_id: sourceSessionId,
setup_id: setupId,
...(portableBinding === null ? {} : {
definition_sha256: portableBinding.definitionSha256,
check_sha256: portableBinding.checkSha256,
}),
}),
signal,
});
const body = await responseBody(response);
if (!response.ok) throw apiError(body, response.status);
const job = decodeObservatoryRecordedJob(body);
if (job.sourceSessionId !== sourceSessionId || job.setupId !== setupId
|| (portableBinding !== null && job.definitionSha256 !== portableBinding.definitionSha256)) {
throw new ObservatoryRecordedJobContractError(
"Расчёт относится к другой сессии, сетапу или версии профиля.",
);
}
return job;
}
export async function retryObservatoryRecordedJobPublication(
jobId: string,
{
signal,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
fetcher?: ObservatoryRecordedJobFetch;
} = {},
): Promise<ObservatoryRecordedJob> {
const response = await request(
fetcher,
`/api/v1/observatory/runs/${encodeURIComponent(jobId)}/publication/retry`,
{
method: "POST",
headers: { Accept: "application/json" },
signal,
},
);
const body = await responseBody(response);
if (!response.ok) throw apiError(body, response.status);
const job = decodeObservatoryRecordedJob(body);
if (job.jobId !== jobId) {
throw new ObservatoryRecordedJobContractError(
"Повтор публикации вернул другой расчёт.",
);
}
return job;
}
export function decodeObservatoryRecordedJob(value: unknown): ObservatoryRecordedJob {
const row = record(value, "расчёт");
exactKeys(row, [
"authority", "checkpoint_policy", "claim_generation", "claim_lease", "created_at_utc", "executor",
"idempotency_key", "identity_sha256", "job_id", "preemption_receipt_sha256",
"preemption_requested", "priority", "publication", "request_sha256", "restart_from_zero", "result",
"schema_version", "setup", "source", "state", "submission_receipt_sha256", "terminal",
"updated_at_utc",
], "расчёт");
exact(row.schema_version, JOB_SCHEMA, "schema_version расчёта");
observationAuthority(row.authority);
digest(row.request_sha256, "request_sha256");
digest(row.identity_sha256, "identity_sha256");
digest(row.submission_receipt_sha256, "submission_receipt_sha256");
const source = record(row.source, "source");
exactKeys(source, [
"adapter", "bundle_sha256", "capability_manifest_sha256", "catalog_sha256", "session_id",
], "source");
digest(source.catalog_sha256, "source.catalog_sha256");
digest(source.bundle_sha256, "source.bundle_sha256");
digest(source.capability_manifest_sha256, "source.capability_manifest_sha256");
const setup = record(row.setup, "setup");
exactKeys(setup, ["definition_id", "definition_sha256", "definition_version", "setup_id"], "setup");
digest(setup.definition_sha256, "setup.definition_sha256");
const state = oneOf(row.state, [
"accepted", "queued", "claimed", "running", "paused", "preemption-pending",
"succeeded", "failed", "reconciliation-required",
] as const, "state");
if (row.claim_lease !== null) {
const lease = record(row.claim_lease, "claim_lease");
exactKeys(
lease,
["claimed_at_utc", "expires_at_utc", "heartbeat_at_utc", "renewal_count"],
"claim_lease",
);
text(lease.claimed_at_utc, "claim_lease.claimed_at_utc");
text(lease.expires_at_utc, "claim_lease.expires_at_utc");
text(lease.heartbeat_at_utc, "claim_lease.heartbeat_at_utc");
nonNegativeInteger(lease.renewal_count, "claim_lease.renewal_count");
}
const result = row.result === null ? null : record(row.result, "result");
if (result !== null) exactKeys(result, ["result_id", "sha256"], "result");
const terminal = row.terminal === null ? null : record(row.terminal, "terminal");
if (terminal !== null) exactKeys(terminal, ["code", "message"], "terminal");
const publication = record(row.publication, "publication");
exactKeys(
publication,
["attempts", "error", "published_at_utc", "state"],
"publication",
);
const publicationState = oneOf(publication.state, [
"not-required", "pending", "failed", "published",
] as const, "publication.state");
const publicationError = publication.error === null
? null
: text(publication.error, "publication.error");
const publishedAtUtc = publication.published_at_utc === null
? null
: text(publication.published_at_utc, "publication.published_at_utc");
return {
jobId: text(row.job_id, "job_id"),
idempotencyKey: text(row.idempotency_key, "idempotency_key"),
sourceSessionId: text(source.session_id, "source.session_id"),
setupId: text(setup.setup_id, "setup.setup_id"),
definitionSha256: String(setup.definition_sha256),
claimGeneration: nonNegativeInteger(row.claim_generation, "claim_generation"),
state,
restartFromZero: boolean(row.restart_from_zero, "restart_from_zero"),
resultId: result === null ? null : text(result.result_id, "result.result_id"),
terminalMessage: terminal === null || terminal.message === null
? null
: text(terminal.message, "terminal.message"),
publication: {
state: publicationState,
attempts: nonNegativeInteger(publication.attempts, "publication.attempts"),
error: publicationError,
publishedAtUtc,
},
createdAtUtc: text(row.created_at_utc, "created_at_utc"),
updatedAtUtc: text(row.updated_at_utc, "updated_at_utc"),
};
}
function observationAuthority(value: unknown): void {
const row = record(value, "authority");
const keys = ["commands_enabled", "actuation_allowed", "navigation_or_safety_accepted", "production_accepted"];
exactKeys(row, keys, "authority");
keys.forEach((key) => exact(row[key], false, `authority.${key}`));
}
async function request(fetcher: ObservatoryRecordedJobFetch, input: string, init: RequestInit): Promise<Response> {
try {
return await fetcher(input, init);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
throw new ObservatoryRecordedJobContractError("Очередь расчётов недоступна.");
}
}
async function responseBody(response: Response): Promise<unknown> {
const body = await response.text();
if (!body) return undefined;
try { return JSON.parse(body) as unknown; } catch { return body; }
}
function apiError(body: unknown, status: number): ObservatoryRecordedJobContractError {
const detail = body && typeof body === "object" && !Array.isArray(body)
? (body as Record<string, unknown>).detail
: null;
return new ObservatoryRecordedJobContractError(
typeof detail === "string" && detail.trim() ? detail : `Observatory API вернул HTTP ${status}.`,
status,
);
}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new ObservatoryRecordedJobContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function array(value: unknown, label: string): unknown[] {
if (!Array.isArray(value)) throw new ObservatoryRecordedJobContractError(`${label}: ожидался массив.`);
return value;
}
function text(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new ObservatoryRecordedJobContractError(`${label}: ожидался текст.`);
}
return value;
}
function digest(value: unknown, label: string): string {
const valueText = text(value, label);
if (!SHA256.test(valueText)) throw new ObservatoryRecordedJobContractError(`${label}: некорректный digest.`);
return valueText;
}
function boolean(value: unknown, label: string): boolean {
if (typeof value !== "boolean") throw new ObservatoryRecordedJobContractError(`${label}: ожидался boolean.`);
return value;
}
function nonNegativeInteger(value: unknown, label: string): number {
if (!Number.isInteger(value) || Number(value) < 0) {
throw new ObservatoryRecordedJobContractError(`${label}: ожидалось целое число.`);
}
return Number(value);
}
function exact<T>(value: unknown, expected: T, label: string): T {
if (value !== expected) throw new ObservatoryRecordedJobContractError(`${label}: значение изменилось.`);
return expected;
}
function oneOf<const T extends readonly string[]>(value: unknown, allowed: T, label: string): T[number] {
if (typeof value !== "string" || !allowed.includes(value)) {
throw new ObservatoryRecordedJobContractError(`${label}: значение не поддерживается.`);
}
return value as T[number];
}
function exactKeys(value: Record<string, unknown>, expected: readonly string[], label: string): void {
const actual = Object.keys(value).sort();
const sortedExpected = [...expected].sort();
if (actual.length !== sortedExpected.length || actual.some((key, index) => key !== sortedExpected[index])) {
throw new ObservatoryRecordedJobContractError(`${label}: обнаружены неизвестные поля.`);
}
}
@@ -0,0 +1,124 @@
import type { ObservatoryRecordedJob } from "./recordedJobs";
const PHASES = {
"source-transfer": "Передаём входные данные",
"source-preparation": "Подготавливаем вход",
computing: "Обрабатываем запись",
"result-assembly": "Собираем результат",
"result-transfer": "Передаём результат",
} as const;
const UNITS = { frames: "кадров", members: "файлов", steps: "операций" } as const;
type Phase = keyof typeof PHASES;
type Unit = keyof typeof UNITS;
export interface RecordedProgressView {
readonly jobId: string;
readonly claimGeneration: number;
readonly sequence: number;
readonly phase: Phase;
readonly unit: Unit;
readonly completed: number;
readonly total: number | null;
readonly ageSeconds: number;
readonly elapsedSeconds: number;
readonly phaseElapsedSeconds: number;
}
export async function fetchRecordedProgress(
job: ObservatoryRecordedJob,
{ signal, fetcher = globalThis.fetch }: {
signal?: AbortSignal;
fetcher?: typeof globalThis.fetch;
} = {},
): Promise<RecordedProgressView | null> {
const request = new AbortController();
const abort = () => request.abort();
signal?.addEventListener("abort", abort, { once: true });
if (signal?.aborted) request.abort();
const timer = globalThis.setTimeout(abort, 2_500);
try {
const response = await fetcher(
`/api/v1/observatory/runs/${encodeURIComponent(job.jobId)}/progress`,
{ signal: request.signal, headers: { Accept: "application/json" } },
);
if (!response.ok) throw new Error("Прогресс расчёта недоступен.");
return decodeRecordedProgress(await response.json(), job);
} finally {
globalThis.clearTimeout(timer);
signal?.removeEventListener("abort", abort);
}
}
export function decodeRecordedProgress(
value: unknown, job: ObservatoryRecordedJob,
): RecordedProgressView | null {
const row = object(value);
keys(row, ["schema_version", "job_id", "source_session_id", "setup_id",
"definition_sha256", "claim_generation", "state", "received_at_utc", "age_seconds", "progress"]);
if (row.schema_version !== "missioncore.observatory-recorded-progress-view/v1"
|| row.job_id !== job.jobId || row.source_session_id !== job.sourceSessionId
|| row.setup_id !== job.setupId || row.definition_sha256 !== job.definitionSha256
|| row.claim_generation !== job.claimGeneration) throw new Error("Прогресс другой попытки.");
if (row.progress === null || row.state !== job.state) return null;
const progress = object(row.progress);
keys(progress, ["schema_version", "claim_generation", "sequence", "phase_index",
"phase", "unit", "completed", "total", "elapsed_seconds", "phase_elapsed_seconds"]);
if (progress.schema_version !== "missioncore.observatory-recorded-progress/v1"
|| progress.claim_generation !== row.claim_generation
|| !(typeof progress.phase === "string" && Object.hasOwn(PHASES, progress.phase))
|| !(typeof progress.unit === "string" && Object.hasOwn(UNITS, progress.unit))
|| typeof row.received_at_utc !== "string" || !Number.isFinite(Date.parse(row.received_at_utc))) {
throw new Error("Некорректный прогресс.");
}
const completed = integer(progress.completed);
const total = progress.total === null ? null : integer(progress.total, 1);
const elapsed = finite(progress.elapsed_seconds);
const phaseElapsed = finite(progress.phase_elapsed_seconds);
if ((total !== null && completed > total) || phaseElapsed > elapsed) {
throw new Error("Некорректные счётчики прогресса.");
}
integer(progress.phase_index);
return {
jobId: job.jobId, claimGeneration: integer(progress.claim_generation, 1),
sequence: integer(progress.sequence, 1), phase: progress.phase as Phase,
unit: progress.unit as Unit, completed, total, ageSeconds: finite(row.age_seconds),
elapsedSeconds: elapsed, phaseElapsedSeconds: phaseElapsed,
};
}
export function recordedProgressLabel(
job: ObservatoryRecordedJob | null, progress: RecordedProgressView | null,
): string {
if (job?.publication.state === "pending") return "Сохраняем результат";
if (!job || job.state === "accepted" || job.state === "queued") return "Ожидаем расчёт";
if (job.state === "failed") return "Ошибка расчёта";
if (job.state === "succeeded") return "Расчёт завершён";
if (job.state === "paused" || job.state === "preemption-pending") return "Расчёт приостановлен";
if (job.state === "reconciliation-required") return "Проверяем состояние расчёта";
if (!progress || progress.jobId !== job.jobId
|| progress.claimGeneration !== job.claimGeneration) return "Ожидаем данные расчёта";
if (progress.ageSeconds > 15) return "Ожидаем обновление прогресса";
const count = progress.total === null
? (progress.completed > 0 ? ` · ${progress.completed.toLocaleString("ru-RU")} ${UNITS[progress.unit]}` : "")
: ` · ${progress.completed.toLocaleString("ru-RU")} / ${progress.total.toLocaleString("ru-RU")} ${UNITS[progress.unit]}`;
return PHASES[progress.phase] + count;
}
function object(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Прогресс: ожидался объект.");
return value as Record<string, unknown>;
}
function keys(row: Record<string, unknown>, expected: string[]): void {
if (Object.keys(row).length !== expected.length || expected.some((key) => !Object.hasOwn(row, key))) {
throw new Error("Прогресс: неизвестные поля.");
}
}
function finite(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error("Прогресс: некорректное число.");
return value;
}
function integer(value: unknown, minimum = 0): number {
const number = finite(value);
if (!Number.isSafeInteger(number) || number < minimum) throw new Error("Прогресс: некорректный счётчик.");
return number;
}
@@ -0,0 +1,335 @@
import type { LaboratoryFetch } from "../laboratory/advancedResults";
import {
fetchVegetationShadowResultMetadata,
type VegetationFullRouteReview,
type VegetationShadowResult,
} from "../laboratory/vegetationShadow";
import type { ObservationLabInstance } from "../observation/sessionArchive";
import {
decodeLabReplayCapability,
decodeCanonicalLabReplayCapabilityProvenance,
decodePortableLabReplayCapabilityProvenance,
ObservationLabReplayCapabilityContractError,
} from "../observation/labReplayCapability";
const CANONICAL_RESULT_ID = /^lab-v1-vegetation-shadow-[a-f0-9]{64}$/;
const CANONICAL_SOURCE_SESSION_ID = "20260828T130511Z_viewer_live";
interface ObservatoryRecordedRunBindingBase {
readonly evidenceSessionId: string;
readonly sourceSessionId: string;
readonly resultId: string;
readonly activation: "explicit";
}
export interface ObservatoryCanonicalRecordedRunBinding
extends ObservatoryRecordedRunBindingBase {
readonly kind: "canonical-recorded-rerun";
readonly viewerProfile: "recorded-session";
readonly timeline: "session_time";
}
export interface ObservatoryPortableResultBinding
extends ObservatoryRecordedRunBindingBase {
readonly kind: "portable-result-review";
readonly viewerProfile: "portable-result";
readonly timeline: "result-defined";
readonly definitionSha256: string;
}
export type ObservatoryRecordedRunBinding =
| ObservatoryCanonicalRecordedRunBinding
| ObservatoryPortableResultBinding;
export interface ObservatoryPortableResultReview {
readonly kind: "portable-result";
readonly resultId: string;
readonly sourceSessionId: string;
readonly resultKind: string;
readonly definitionSha256: string;
readonly calculationProfile: Readonly<Record<string, unknown>> | null;
readonly artifactManifestId: string;
readonly artifacts: readonly {
readonly role: string;
readonly mediaType: string;
readonly sha256: string;
readonly byteLength: number;
}[];
readonly resultDocument: Readonly<Record<string, unknown>>;
}
export type ObservatoryRecordedRunReview =
| { readonly kind: "canonical-recorded-rerun"; readonly review: VegetationFullRouteReview }
| ObservatoryPortableResultReview;
export class ObservatoryRecordedRunContractError extends Error {
constructor(message: string) {
super(message);
this.name = "ObservatoryRecordedRunContractError";
}
}
export function observatoryRecordedRunBinding(
evidenceSessionId: string,
lab: ObservationLabInstance,
): ObservatoryRecordedRunBinding | null {
const capability = lab.replayCapability;
if (!capability) return null;
if (capability.kind === "portable-result-review") {
if (
evidenceSessionId !== lab.resultId
|| lab.sourceSessionId === evidenceSessionId
|| lab.configSha256 === null
|| !/^[a-f0-9]{64}$/.test(lab.configSha256)
|| capability.viewerProfile !== "portable-result"
|| capability.timeline !== "result-defined"
|| capability.activation !== "explicit"
|| capability.commandsEnabled !== false
) {
throw new ObservatoryRecordedRunContractError(
`Portable-результат ${lab.resultId} не допущен к универсальному viewer.`,
);
}
try {
decodePortableLabReplayCapabilityProvenance(
lab.provenance,
evidenceSessionId,
lab.resultId,
lab.sourceSessionId,
lab.configSha256,
);
} catch (error) {
if (!(error instanceof ObservationLabReplayCapabilityContractError)) throw error;
throw new ObservatoryRecordedRunContractError(error.message);
}
return {
kind: "portable-result-review",
evidenceSessionId,
sourceSessionId: lab.sourceSessionId,
resultId: lab.resultId,
viewerProfile: "portable-result",
timeline: "result-defined",
activation: "explicit",
definitionSha256: lab.configSha256,
};
}
try {
decodeCanonicalLabReplayCapabilityProvenance(
lab.provenance,
evidenceSessionId,
lab.resultId,
);
} catch (error) {
if (!(error instanceof ObservationLabReplayCapabilityContractError)) throw error;
throw new ObservatoryRecordedRunContractError(error.message);
}
if (
evidenceSessionId !== lab.resultId
|| lab.sourceSessionId !== CANONICAL_SOURCE_SESSION_ID
|| lab.labId !== "LAB V1"
|| lab.resultKind !== "recorded-perception-qualification"
|| !CANONICAL_RESULT_ID.test(lab.resultId)
|| lab.configSha256 !== null
|| lab.sourceResultId === null
|| !CANONICAL_RESULT_ID.test(lab.sourceResultId)
|| capability.kind !== "canonical-recorded-rerun"
|| capability.viewerProfile !== "recorded-session"
|| capability.timeline !== "session_time"
|| capability.activation !== "explicit"
|| capability.commandsEnabled !== false
) {
throw new ObservatoryRecordedRunContractError(
`LAB-результат ${lab.resultId} не допущен к каноническому recorded replay.`,
);
}
return {
kind: "canonical-recorded-rerun",
evidenceSessionId,
sourceSessionId: lab.sourceSessionId,
resultId: lab.resultId,
viewerProfile: "recorded-session",
timeline: "session_time",
activation: "explicit",
};
}
export async function fetchObservatoryRecordedRunReview(
binding: ObservatoryRecordedRunBinding,
{
selectedSourceSessionId,
fetcher = fetch,
signal,
}: {
selectedSourceSessionId: string;
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
},
): Promise<ObservatoryRecordedRunReview> {
if (binding.sourceSessionId !== selectedSourceSessionId) {
throw new ObservatoryRecordedRunContractError(
"Запуск не связан с выбранной исходной сессией.",
);
}
if (binding.kind === "portable-result-review") {
return fetchPortableResultReview(binding, { fetcher, signal });
}
const result = await fetchVegetationShadowResultMetadata(binding.resultId, {
fetcher,
signal,
});
return {
kind: "canonical-recorded-rerun",
review: admitObservatoryRecordedRunReview(
binding,
selectedSourceSessionId,
result,
),
};
}
export function admitObservatoryRecordedRunReview(
binding: ObservatoryCanonicalRecordedRunBinding,
selectedSourceSessionId: string,
result: VegetationShadowResult,
): VegetationFullRouteReview {
if (binding.sourceSessionId !== selectedSourceSessionId) {
throw new ObservatoryRecordedRunContractError(
"Запуск не связан с выбранной исходной сессией.",
);
}
if (result.resultId !== binding.resultId) {
throw new ObservatoryRecordedRunContractError(
"Запечатанный результат не совпадает с выбранным запуском.",
);
}
const review = result.routeFullReview;
if (!review || review.sessionId !== binding.sourceSessionId) {
throw new ObservatoryRecordedRunContractError(
"Запечатанный результат потерял точную связь с исходной сессией.",
);
}
return review;
}
async function fetchPortableResultReview(
binding: ObservatoryPortableResultBinding,
{
fetcher,
signal,
}: {
fetcher: LaboratoryFetch;
signal?: AbortSignal;
},
): Promise<ObservatoryPortableResultReview> {
const response = await fetcher(
`/api/v1/observatory/portable-results/${encodeURIComponent(binding.resultId)}`,
{ headers: { Accept: "application/json" }, signal },
);
let body: unknown;
try {
body = await response.json();
} catch {
throw new ObservatoryRecordedRunContractError(
"Portable viewer получил некорректный ответ сервера.",
);
}
if (!response.ok) {
throw new ObservatoryRecordedRunContractError(
isRecord(body) && typeof body.detail === "string"
? body.detail
: `Portable viewer вернул HTTP ${response.status}.`,
);
}
return decodePortableResultReview(body, binding);
}
function decodePortableResultReview(
value: unknown,
binding: ObservatoryPortableResultBinding,
): ObservatoryPortableResultReview {
if (!isRecord(value)) {
throw new ObservatoryRecordedRunContractError("Portable viewer вернул не объект.");
}
const expected = new Set([
"schema_version",
"result_id",
"source_session_id",
"result_kind",
"definition_sha256",
"viewer_capability",
"calculation_profile",
"artifact_manifest_id",
"artifacts",
"result_document",
]);
if (Object.keys(value).some((key) => !expected.has(key)) || Object.keys(value).length !== expected.size) {
throw new ObservatoryRecordedRunContractError("Portable viewer изменил контракт ответа.");
}
let viewerCapability;
try {
viewerCapability = decodeLabReplayCapability(
value.viewer_capability,
binding.evidenceSessionId,
);
} catch (error) {
if (!(error instanceof ObservationLabReplayCapabilityContractError)) throw error;
throw new ObservatoryRecordedRunContractError(error.message);
}
if (
value.schema_version !== "missioncore.observatory-portable-result-view/v1"
|| value.result_id !== binding.resultId
|| value.source_session_id !== binding.sourceSessionId
|| value.definition_sha256 !== binding.definitionSha256
|| typeof value.result_kind !== "string"
|| typeof value.artifact_manifest_id !== "string"
|| !/^[a-f0-9]{64}$/.test(value.artifact_manifest_id)
|| !Array.isArray(value.artifacts)
|| !isRecord(value.result_document)
|| !isRecord(value.calculation_profile)
|| viewerCapability === null
|| viewerCapability.kind !== "portable-result-review"
|| viewerCapability.viewerProfile !== binding.viewerProfile
|| viewerCapability.timeline !== binding.timeline
) {
throw new ObservatoryRecordedRunContractError("Portable viewer потерял identity результата.");
}
const artifacts = value.artifacts.map((item) => {
if (
!isRecord(item)
|| Object.keys(item).length !== 4
|| !["role", "media_type", "sha256", "byte_length"].every(
(key) => Object.prototype.hasOwnProperty.call(item, key),
)
|| typeof item.role !== "string"
|| typeof item.media_type !== "string"
|| typeof item.sha256 !== "string"
|| !/^[a-f0-9]{64}$/.test(item.sha256)
|| typeof item.byte_length !== "number"
|| !Number.isSafeInteger(item.byte_length)
|| item.byte_length < 0
) {
throw new ObservatoryRecordedRunContractError("Portable viewer получил неверный artifact.");
}
return {
role: item.role,
mediaType: item.media_type,
sha256: item.sha256,
byteLength: item.byte_length,
};
});
return {
kind: "portable-result",
resultId: binding.resultId,
sourceSessionId: binding.sourceSessionId,
resultKind: value.result_kind,
definitionSha256: binding.definitionSha256,
calculationProfile: value.calculation_profile,
artifactManifestId: value.artifact_manifest_id,
artifacts,
resultDocument: value.result_document,
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
@@ -0,0 +1,98 @@
import { useCallback, useEffect, useState } from "react";
import {
AI_MODULE_SETUP_IDS,
fetchAIModuleCatalog,
fetchAICompositionJobs,
fetchAICompositionRuns,
type AICompositionRun,
} from "./aiComposition";
import { fetchRecordedProgress, type RecordedProgressView } from "./recordedProgress";
import type { ObservatoryRecordedJob } from "./recordedJobs";
const OPEN = new Set(["accepted", "queued", "claimed", "running", "paused",
"preemption-pending", "reconciliation-required"]);
interface Snapshot {
readonly sourceSessionId: string;
readonly jobs: readonly ObservatoryRecordedJob[];
readonly runs: readonly AICompositionRun[];
readonly moduleLabelsBySetup: Readonly<Record<string, string>>;
readonly progress: Readonly<Record<string, RecordedProgressView>>;
readonly error: string | null;
}
export function useAICompositionJobs(sourceSessionId: string) {
const [revision, setRevision] = useState(0);
const [snapshot, setSnapshot] = useState<Snapshot>({
sourceSessionId: "", jobs: [], runs: [], moduleLabelsBySetup: {}, progress: {}, error: null,
});
const current = snapshot.sourceSessionId === sourceSessionId ? snapshot : {
sourceSessionId, jobs: [], runs: [], moduleLabelsBySetup: {}, progress: {}, error: null,
};
useEffect(() => {
if (!sourceSessionId) {
setSnapshot({ sourceSessionId, jobs: [], runs: [], moduleLabelsBySetup: {}, progress: {}, error: null });
return;
}
const request = new AbortController();
void Promise.all([
fetchAICompositionJobs(sourceSessionId, request.signal),
fetchAICompositionRuns(sourceSessionId, request.signal),
fetchAIModuleCatalog(request.signal),
]).then(async ([jobs, runs, catalog]) => {
const active = jobs.filter((job) => OPEN.has(job.state));
const samples = await Promise.all(active.map(async (job) => [
job.jobId, await fetchRecordedProgress(job, { signal: request.signal }).catch(() => null),
] as const));
if (request.signal.aborted) return;
const catalogLabels = catalog.groups.flatMap(({ modules }) => (
modules.flatMap((module) => {
const setupId = AI_MODULE_SETUP_IDS[module.moduleId];
return setupId ? [[setupId, module.label] as const] : [];
})
));
const projectedLabels = runs.flatMap((run) => run.presentation.modules.flatMap((module) => {
const setupId = AI_MODULE_SETUP_IDS[module.moduleId];
return setupId ? [[setupId, module.label] as const] : [];
}));
setSnapshot({
sourceSessionId, jobs, runs,
moduleLabelsBySetup: Object.fromEntries([...catalogLabels, ...projectedLabels]),
progress: Object.fromEntries(samples.filter(
(sample): sample is readonly [string, RecordedProgressView] => sample[1] !== null,
)),
error: null,
});
}).catch((error: unknown) => {
if (request.signal.aborted) return;
setSnapshot((value) => ({
sourceSessionId, jobs: value.sourceSessionId === sourceSessionId ? value.jobs : [],
runs: value.sourceSessionId === sourceSessionId ? value.runs : [],
moduleLabelsBySetup: value.sourceSessionId === sourceSessionId
? value.moduleLabelsBySetup : {},
progress: value.sourceSessionId === sourceSessionId ? value.progress : {},
error: error instanceof Error ? error.message : "Очередь AI-слоёв недоступна.",
}));
});
return () => request.abort();
}, [revision, sourceSessionId]);
const active = current.runs.some((run) => run.state === "running")
|| current.jobs.some((job) => OPEN.has(job.state) || job.publication.state === "pending");
useEffect(() => {
if (!active) return;
const timer = globalThis.setTimeout(() => setRevision((value) => value + 1), 1_500);
return () => globalThis.clearTimeout(timer);
}, [active, revision]);
return {
jobs: current.jobs,
runs: current.runs,
moduleLabelsBySetup: current.moduleLabelsBySetup,
progress: current.progress,
error: current.error,
refresh: useCallback(() => setRevision((value) => value + 1), []),
};
}
@@ -0,0 +1,148 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
applyObservatoryCatalogMutationOverlay,
fetchObservatoryCatalog,
reconcileObservatoryCatalogMutationOverlay,
type ObservatoryCatalog,
type ObservatoryCatalogMutationOverlay,
} from "./catalog";
export type ObservatoryCatalogState =
| "idle"
| "loading"
| "ready"
| "refreshing"
| "error";
export interface ObservatoryCatalogController {
readonly catalog: ObservatoryCatalog | null;
readonly state: ObservatoryCatalogState;
readonly error: string | null;
readonly refresh: () => Promise<ObservatoryCatalog | null>;
readonly applyEvidenceRename: (
sessionId: string,
displayName: string,
) => void;
readonly applyEvidenceDeletion: (sessionId: string) => void;
}
interface ActiveCatalogRequest {
readonly id: number;
readonly controller: AbortController;
}
type ObservatoryCatalogMutationDraft =
| { readonly kind: "rename"; readonly displayName: string }
| { readonly kind: "delete" };
function errorMessage(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "Каталог Обсерватории недоступен.";
}
function isAbortError(error: unknown): boolean {
return error instanceof DOMException && error.name === "AbortError";
}
export function useObservatoryCatalog(): ObservatoryCatalogController {
const [catalog, setCatalog] = useState<ObservatoryCatalog | null>(null);
const [state, setState] = useState<ObservatoryCatalogState>("idle");
const [error, setError] = useState<string | null>(null);
const catalogRef = useRef<ObservatoryCatalog | null>(null);
const overlayRef = useRef<ObservatoryCatalogMutationOverlay>(new Map());
const mutationRevisionRef = useRef(0);
const requestSequenceRef = useRef(0);
const activeRequestRef = useRef<ActiveCatalogRequest | null>(null);
const loadCatalog = useCallback(async (): Promise<ObservatoryCatalog | null> => {
const id = requestSequenceRef.current + 1;
requestSequenceRef.current = id;
activeRequestRef.current?.controller.abort();
const controller = new AbortController();
activeRequestRef.current = { id, controller };
const requestMutationRevision = mutationRevisionRef.current;
setState(catalogRef.current ? "refreshing" : "loading");
setError(null);
try {
const serverCatalog = await fetchObservatoryCatalog({
signal: controller.signal,
});
if (controller.signal.aborted || requestSequenceRef.current !== id) {
return null;
}
const reconciled = reconcileObservatoryCatalogMutationOverlay(
serverCatalog,
overlayRef.current,
requestMutationRevision,
);
overlayRef.current = reconciled.overlay;
catalogRef.current = reconciled.catalog;
activeRequestRef.current = null;
setCatalog(reconciled.catalog);
setState("ready");
return reconciled.catalog;
} catch (loadError: unknown) {
if (
controller.signal.aborted
|| requestSequenceRef.current !== id
|| isAbortError(loadError)
) {
return null;
}
activeRequestRef.current = null;
setError(errorMessage(loadError));
setState("error");
return null;
}
}, []);
useEffect(() => {
void loadCatalog();
return () => {
requestSequenceRef.current += 1;
activeRequestRef.current?.controller.abort();
activeRequestRef.current = null;
};
}, [loadCatalog]);
const applyMutation = useCallback((
sessionId: string,
mutation: ObservatoryCatalogMutationDraft,
) => {
const revision = mutationRevisionRef.current + 1;
mutationRevisionRef.current = revision;
const overlay = new Map(overlayRef.current);
overlay.set(sessionId, { ...mutation, revision });
overlayRef.current = overlay;
if (!catalogRef.current) return;
const projected = applyObservatoryCatalogMutationOverlay(
catalogRef.current,
overlay,
);
catalogRef.current = projected;
setCatalog(projected);
}, []);
const applyEvidenceRename = useCallback((
sessionId: string,
displayName: string,
) => {
applyMutation(sessionId, { kind: "rename", displayName });
}, [applyMutation]);
const applyEvidenceDeletion = useCallback((sessionId: string) => {
applyMutation(sessionId, { kind: "delete" });
}, [applyMutation]);
return {
catalog,
state,
error,
refresh: loadCatalog,
applyEvidenceRename,
applyEvidenceDeletion,
};
}
@@ -0,0 +1,132 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
fetchObservatoryPortableLaboratorySetups,
preflightObservatoryLaboratorySetup,
selectableObservatorySetups,
type ObservatoryLaboratoryRunPreflight,
type ObservatoryLaboratorySetupCatalog,
} from "./laboratorySetups";
type SetupCatalogState = "idle" | "loading" | "ready" | "refreshing" | "error";
export type SetupPreflightState =
| { readonly kind: "idle" }
| { readonly kind: "checking" }
| { readonly kind: "ready"; readonly value: ObservatoryLaboratoryRunPreflight }
| { readonly kind: "error"; readonly message: string };
export function useObservatoryLaboratorySetups(sourceSessionId: string) {
const [catalog, setCatalog] = useState<ObservatoryLaboratorySetupCatalog | null>(null);
const [state, setState] = useState<SetupCatalogState>("idle");
const [error, setError] = useState<string | null>(null);
const [selectedSetupId, setSelectedSetupId] = useState("");
const [revision, setRevision] = useState(0);
const [preflight, setPreflight] = useState<SetupPreflightState>({ kind: "idle" });
const requestSequence = useRef(0);
const preflightRequest = useRef<AbortController | null>(null);
const activeCatalog = catalog?.sourceSessionId === sourceSessionId ? catalog : null;
const selectableSetups = useMemo(() => selectableObservatorySetups(activeCatalog), [activeCatalog]);
const selectedSetup = selectableSetups.find((setup) => setup.setupId === selectedSetupId) ?? null;
const selectedDefinitionSha256 = selectedSetup?.runDefinition?.definitionSha256 ?? null;
useEffect(() => {
preflightRequest.current?.abort();
preflightRequest.current = null;
if (!sourceSessionId) {
setCatalog(null);
setState("idle");
setError(null);
setSelectedSetupId("");
setPreflight({ kind: "idle" });
return;
}
const sequence = ++requestSequence.current;
const request = new AbortController();
setState((current) => activeCatalog && current !== "idle" ? "refreshing" : "loading");
setError(null);
setPreflight({ kind: "idle" });
// Archived definitions belong to evidence, not to the calculation selector.
void fetchObservatoryPortableLaboratorySetups(sourceSessionId, { signal: request.signal })
.then((next) => {
if (request.signal.aborted || requestSequence.current !== sequence) return;
setCatalog(next);
const selectable = selectableObservatorySetups(next);
setSelectedSetupId((current) => (
selectable.some((setup) => setup.setupId === current)
? current
: selectable[0]?.setupId ?? ""
));
setState("ready");
})
.catch((caught: unknown) => {
if (request.signal.aborted || requestSequence.current !== sequence) return;
setState("error");
setError(caught instanceof Error && caught.message.trim()
? caught.message
: "Каталог профилей недоступен.");
});
return () => request.abort();
}, [revision, sourceSessionId]);
useEffect(() => () => preflightRequest.current?.abort(), []);
useEffect(() => {
preflightRequest.current?.abort();
preflightRequest.current = null;
setPreflight((current) => current.kind === "idle" ? current : { kind: "idle" });
}, [selectedSetupId, sourceSessionId, selectedDefinitionSha256]);
const selectSetup = useCallback((setupId: string) => {
preflightRequest.current?.abort();
preflightRequest.current = null;
setSelectedSetupId(setupId);
setPreflight({ kind: "idle" });
}, []);
const refresh = useCallback(() => setRevision((value) => value + 1), []);
const check = useCallback(async () => {
if (!sourceSessionId || !selectedSetup || preflight.kind === "checking") return;
preflightRequest.current?.abort();
const request = new AbortController();
preflightRequest.current = request;
setPreflight({ kind: "checking" });
try {
const value = await preflightObservatoryLaboratorySetup(
sourceSessionId,
selectedSetup,
{ signal: request.signal },
);
if (request.signal.aborted || preflightRequest.current !== request) return;
setPreflight({ kind: "ready", value });
} catch (caught) {
if (request.signal.aborted || preflightRequest.current !== request) return;
setPreflight({
kind: "error",
message: caught instanceof Error && caught.message.trim()
? caught.message
: "Не удалось проверить возможность запуска.",
});
} finally {
if (preflightRequest.current === request) preflightRequest.current = null;
}
}, [preflight.kind, selectedSetup, sourceSessionId]);
useEffect(() => {
if (state !== "ready" || !selectedSetup || preflight.kind !== "idle") return;
void check();
}, [check, preflight.kind, selectedSetup, state]);
return {
catalog: activeCatalog,
selectableSetups,
state,
error,
selectedSetupId: selectedSetup?.setupId ?? "",
selectedSetup,
selectSetup,
refresh,
preflight,
check,
};
}
@@ -0,0 +1,207 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { fetchRecordedProgress, type RecordedProgressView } from "./recordedProgress";
import {
fetchObservatoryRecordedJobs,
retryObservatoryRecordedJobPublication,
submitObservatoryRecordedJob,
type ObservatoryRecordedJob,
} from "./recordedJobs";
type RecordedJobsState =
| "idle"
| "loading"
| "ready"
| "refreshing"
| "submitting"
| "retrying-publication"
| "error";
interface JobSnapshot {
readonly selectionKey: string;
readonly jobs: readonly ObservatoryRecordedJob[];
readonly state: RecordedJobsState;
readonly error: string | null;
readonly progress?: RecordedProgressView | null;
}
const EMPTY_JOBS = [] as const;
const OPEN_STATES = new Set([
"accepted", "queued", "claimed", "running", "paused", "preemption-pending",
"reconciliation-required",
]);
const POLL_INTERVAL_MS = 1_500;
export function useObservatoryRecordedJobs(
sourceSessionId: string,
setupId: string,
definitionSha256: string,
) {
const selectionKey = JSON.stringify([sourceSessionId, setupId, definitionSha256]);
const [snapshot, setSnapshot] = useState<JobSnapshot>({
selectionKey: "", jobs: EMPTY_JOBS, state: "idle", error: null,
});
const [revision, setRevision] = useState(0);
const requestRef = useRef<AbortController | null>(null);
const requestSequence = useRef(0);
const idempotencyKeys = useRef(new Map<string, string>());
const observedJobId = useRef<string | null>(null);
const hasSelection = Boolean(sourceSessionId && setupId && definitionSha256);
// A source/profile change must not display even one render of the previous queue.
const current = snapshot.selectionKey === selectionKey ? snapshot : null;
const jobs = current?.jobs ?? EMPTY_JOBS;
const state = current?.state ?? (hasSelection ? "loading" : "idle");
const error = current?.error ?? null;
useEffect(() => {
requestRef.current?.abort();
requestRef.current = null;
if (!hasSelection) {
setSnapshot({ selectionKey, jobs: EMPTY_JOBS, state: "idle", error: null });
return;
}
const sequence = ++requestSequence.current;
const request = new AbortController();
requestRef.current = request;
setSnapshot({
selectionKey, jobs,
state: jobs.length > 0 ? "refreshing" : "loading", error: null,
progress: current?.progress ?? null,
});
void fetchObservatoryRecordedJobs(sourceSessionId, setupId, {
definitionSha256, signal: request.signal,
}).then(async (next) => {
if (request.signal.aborted || requestSequence.current !== sequence) return;
const pending = next.find((job) => OPEN_STATES.has(job.state));
const progress = pending
? await fetchRecordedProgress(pending, { signal: request.signal }).catch(() => null)
: null;
if (request.signal.aborted || requestSequence.current !== sequence) return;
if (pending) observedJobId.current = pending.jobId;
setSnapshot({ selectionKey, jobs: next, state: "ready", error: null, progress });
}).catch((caught: unknown) => {
if (request.signal.aborted || requestSequence.current !== sequence) return;
setSnapshot({
selectionKey, jobs, state: "error",
error: caught instanceof Error && caught.message.trim()
? caught.message
: "Очередь расчётов недоступна.",
});
}).finally(() => {
if (requestRef.current === request) requestRef.current = null;
});
return () => request.abort();
}, [revision, selectionKey]);
useEffect(() => () => requestRef.current?.abort(), []);
const latestJob = jobs[0] ?? null;
const activeJob = jobs.find((job) => OPEN_STATES.has(job.state)) ?? null;
const publicationPending = jobs.some((job) => job.publication.state === "pending");
// Do not promote historical terminal logs to a new operator action's error.
const computationFailed = latestJob?.state === "failed"
&& latestJob.jobId === observedJobId.current;
useEffect(() => {
if ((state !== "ready" && state !== "error") || (!activeJob && !publicationPending)) return;
const timer = globalThis.setTimeout(
() => setRevision((value) => value + 1),
POLL_INTERVAL_MS,
);
return () => globalThis.clearTimeout(timer);
}, [activeJob, publicationPending, revision, state]);
useEffect(() => {
if (!latestJob || activeJob) return;
idempotencyKeys.current.delete(selectionKey);
}, [activeJob, latestJob, selectionKey]);
const refresh = useCallback(() => setRevision((value) => value + 1), []);
const submit = useCallback(async (
portableBinding: {
readonly definitionSha256: string;
readonly checkSha256: string;
} | null = null,
): Promise<ObservatoryRecordedJob | null> => {
if (!hasSelection || state !== "ready"
|| portableBinding?.definitionSha256 !== definitionSha256
|| publicationPending || latestJob?.publication.state === "failed") return null;
if (activeJob) return activeJob;
requestRef.current?.abort();
const request = new AbortController();
requestRef.current = request;
const key = idempotencyKeys.current.get(selectionKey) ?? createIdempotencyKey();
idempotencyKeys.current.set(selectionKey, key);
setSnapshot({ selectionKey, jobs, state: "submitting", error: null });
try {
const job = await submitObservatoryRecordedJob(
sourceSessionId, setupId, key, portableBinding, { signal: request.signal },
);
if (request.signal.aborted || requestRef.current !== request) return null;
observedJobId.current = job.jobId;
setSnapshot({
selectionKey, jobs: [job, ...jobs.filter((candidate) => candidate.jobId !== job.jobId)],
state: "ready", error: null,
});
return job;
} catch (caught) {
if (request.signal.aborted || requestRef.current !== request) return null;
setSnapshot({
selectionKey, jobs, state: "error",
error: caught instanceof Error && caught.message.trim()
? caught.message
: "Не удалось поставить расчёт в очередь.",
});
return null;
} finally {
if (requestRef.current === request) requestRef.current = null;
}
}, [activeJob, definitionSha256, hasSelection, jobs, latestJob,
publicationPending, selectionKey, setupId, sourceSessionId, state]);
const retryPublication = useCallback(async (): Promise<ObservatoryRecordedJob | null> => {
if (!latestJob || latestJob.publication.state !== "failed"
|| state === "retrying-publication") return null;
requestRef.current?.abort();
const request = new AbortController();
requestRef.current = request;
setSnapshot({ selectionKey, jobs, state: "retrying-publication", error: null });
try {
const job = await retryObservatoryRecordedJobPublication(latestJob.jobId, {
signal: request.signal,
});
if (request.signal.aborted || requestRef.current !== request) return null;
setSnapshot({
selectionKey, jobs: [job, ...jobs.filter((candidate) => candidate.jobId !== job.jobId)],
state: "ready", error: null,
});
return job;
} catch (caught) {
if (request.signal.aborted || requestRef.current !== request) return null;
setSnapshot({
selectionKey, jobs, state: "error",
error: caught instanceof Error && caught.message.trim()
? caught.message
: "Не удалось повторить публикацию результата.",
});
return null;
} finally {
if (requestRef.current === request) requestRef.current = null;
}
}, [jobs, latestJob, selectionKey, state]);
return {
jobs, latestJob, activeJob, publicationPending, computationFailed,
progress: current?.progress ?? null,
state, error, refresh, submit, retryPublication,
};
}
function createIdempotencyKey(): string {
const entropy = typeof globalThis.crypto?.randomUUID === "function"
? globalThis.crypto.randomUUID()
: `${Date.now().toString(36)}-${Math.random().toString(16).slice(2)}`;
return `observatory-ui:${entropy}`;
}
export type ObservatoryRecordedJobsController = ReturnType<typeof useObservatoryRecordedJobs>;
@@ -38,6 +38,8 @@ export function useWorkerTelemetry(
setError(null);
return;
}
setTelemetry(null);
setError(null);
const controller = new AbortController();
const stopPolling = startSequentialPolling(async () => {
setLoading(true);
@@ -48,6 +50,7 @@ export function useWorkerTelemetry(
setError(null);
} catch (reason: unknown) {
if (controller.signal.aborted) return;
setTelemetry(null);
setError(
reason instanceof Error
? reason.message
@@ -114,6 +114,9 @@ export interface WorkerTelemetry {
power_watts?: number;
temperature_celsius?: number;
memory_used_percent?: number;
sm_clock_mhz?: number | null;
memory_clock_mhz?: number | null;
driver_version?: string | null;
} | null;
triton: {
ready: boolean;
@@ -131,6 +134,13 @@ export interface WorkerTelemetry {
completed_runs: number | null;
failed_runs: number | null;
model_load_seconds: number | null;
profile_name?: string | null;
input_state?: string | null;
wait_reason?: string | null;
live_children?: number | null;
input_pauses?: number | null;
pending_bundles?: number | null;
buffer_bytes?: number | null;
stages: WorkerPipelineStage[];
};
network: {
@@ -189,7 +199,9 @@ export async function fetchWorkerTelemetry(
`/api/v1/system/contours/${encodeURIComponent(contourId)}/telemetry?history=90`,
{
method: "GET",
signal,
signal: signal
? AbortSignal.any([signal, AbortSignal.timeout(5_000)])
: AbortSignal.timeout(5_000),
},
),
"missioncore.worker-telemetry/v1",
+13 -1
View File
@@ -23,7 +23,8 @@ export type WorkspaceKind =
| "datasets"
| "artifact-health"
| "lab-archive"
| "simulations";
| "simulations"
| "observatory";
export type CapabilityStatus = "active" | "ready" | "contract" | "later";
@@ -166,6 +167,17 @@ export const workspaces: WorkspaceDefinition[] = [
kind: "simulations",
groups: [],
},
{
id: "observatory",
root: "polygon",
label: "Обсерватория",
title: "AI inference",
eyebrow: "ТЕСТОВЫЙ КОНТУР / ОБСЕРВАТОРИЯ",
description: "Сессии и квалификация без доступа к управлению.",
icon: "eye",
kind: "observatory",
groups: [],
},
{
id: "contour-health",
root: "fleet",
+1
View File
@@ -21,6 +21,7 @@
@import "./styles/device.css";
@import "./styles/responsive.css";
@import "./styles/observation.css";
@import "./styles/observatory.css";
@import "./styles/environment-settings.css";
@import "./styles/system-telemetry.css";
@import "./styles/artifact-health.css";
@@ -62,6 +62,43 @@
pointer-events: auto;
}
.canonical-vegetation-rerun-replay {
width: 100%;
min-width: 0;
}
.canonical-vegetation-rerun-replay:not([data-presentation-state="ready"])
.m4-replay-threat-visual__pane-toolbar,
.canonical-vegetation-rerun-replay:not([data-presentation-state="ready"])
.laboratory-evidence-viewer__controls,
.canonical-vegetation-rerun-replay:not([data-presentation-state="ready"])
.laboratory-evidence-viewer__transport {
visibility: hidden;
pointer-events: none;
}
.canonical-vegetation-rerun-replay[data-presentation-state="loading"]
.m4-replay-threat-visual__unified-content {
visibility: hidden;
}
.canonical-vegetation-rerun-replay__viewport-lock {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.canonical-vegetation-rerun-replay__loading {
position: absolute;
z-index: 6;
inset: 0;
display: grid;
place-items: center;
background: var(--nodedc-canvas);
pointer-events: none;
}
.m4-replay-threat-visual__deck[data-empty="true"] > .l3-visual-audit__state {
position: absolute;
z-index: 1;
@@ -108,29 +145,16 @@
display: flex;
min-width: 0;
align-items: center;
align-content: flex-start;
flex-wrap: wrap;
gap: 0.35rem;
pointer-events: none;
}
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] {
justify-content: flex-end;
}
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"] {
flex-wrap: wrap;
}
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"]
> .m4-replay-threat-visual__pane-layer-controls {
flex: 1 0 100%;
justify-content: flex-start;
}
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"][data-multi-semantic="true"]
> .m4-replay-threat-visual__pane-mode-controls {
margin-left: auto;
}
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]:has(
.m4-replay-threat-visual__review-controls
) .m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] {
@@ -138,12 +162,17 @@
}
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="spatial"] {
justify-content: space-between;
justify-content: flex-start;
}
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="spatial"] {
right: 3.8rem;
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="spatial"],
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]
.m4-replay-threat-visual__deck:not([data-split="true"])
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="media"] {
/* The two viewer-wide actions stay at the deck's right edge. Keep their
complete hit area out of this viewport-owned toolbar at every split. */
right: 7rem;
}
.m4-replay-threat-visual__pane-toolbar > *,
@@ -152,18 +181,15 @@
}
.m4-replay-threat-visual__spatial-toolbar-end {
display: flex;
min-width: 0;
align-items: center;
justify-content: flex-end;
gap: 0.35rem;
overflow-x: auto;
overscroll-behavior-inline: contain;
scrollbar-width: none;
display: contents;
}
.m4-replay-threat-visual__spatial-toolbar-end::-webkit-scrollbar {
display: none;
.m4-replay-threat-visual__pane-toolbar > .m4-replay-threat-visual__pane-layer-controls,
.m4-replay-threat-visual__spatial-toolbar-end > .m4-replay-threat-visual__pane-layer-controls {
/* A viewport toolbar is one adaptive flex sequence. `display: contents`
keeps the semantic layer group while allowing each control to move to the
next row only when the viewport itself runs out of width. */
display: contents;
}
.m4-replay-threat-visual__pane-mode-controls {
@@ -212,7 +238,8 @@
display: flex;
align-items: center;
gap: 0.4rem;
border: 1px solid var(--nodedc-glass-outline);
border: 0;
outline: 0;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-floating-surface);
padding: 0.42rem 0.58rem;
@@ -233,6 +260,15 @@
pointer-events: none;
}
.canonical-vegetation-rerun-replay
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]
.laboratory-evidence-viewer__controls {
right: 0.6rem;
left: auto;
width: auto;
justify-content: flex-end;
}
.m4-replay-threat-evidence-viewer[data-mode-controls="content"] .laboratory-evidence-viewer__controls > .nodedc-icon-button {
pointer-events: auto;
}
@@ -257,6 +293,30 @@
scrollbar-width: none;
}
.m4-replay-threat-visual__layer-with-settings {
display: inline-flex;
flex: none;
align-items: center;
gap: 0.18rem;
border: 0;
border-radius: 999px;
background: var(--nodedc-glass-control-bg);
padding: 0.12rem;
}
.m4-replay-threat-visual__layer-with-settings > .nodedc-icon-button {
width: 1.75rem;
height: 1.75rem;
border: 0;
outline: 0;
box-shadow: none;
}
.m4-replay-threat-visual__layer-with-settings > .nodedc-icon-button:focus-visible {
background: var(--nodedc-focus-surface);
box-shadow: inset 0 0 0 1px rgb(var(--nodedc-accent-rgb) / 0.22);
}
.m4-replay-threat-visual__review-controls,
.m4-replay-threat-visual__review-controls > *,
.m4-replay-threat-visual__single-pane-controls,
@@ -282,6 +342,10 @@
grid-template-columns: auto auto auto minmax(0, 1fr) auto;
}
.canonical-vegetation-rerun-replay__timeline .observation-timeline__playback {
grid-template-columns: auto auto minmax(0, 1fr) auto;
}
.m4-replay-threat-visual__overlay {
box-sizing: border-box;
min-width: 0;
@@ -0,0 +1,640 @@
.observatory-workspace {
display: grid;
grid-auto-rows: max-content;
align-content: start;
gap: 1rem;
min-width: 0;
min-height: 100%;
padding: 1rem;
container-name: observatory-workspace;
container-type: inline-size;
}
.observatory-catalog-bar,
.observatory-catalog-bar__controls,
.observatory-notice,
.observatory-evidence > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.observatory-notice {
flex-wrap: wrap;
}
.observatory-catalog-bar__copy,
.observatory-evidence header > div {
min-width: 0;
}
.observatory-catalog-bar h3,
.observatory-session-summary h3,
.observatory-evidence h3 {
margin: 0.3rem 0 0;
}
.observatory-catalog-bar h3,
.observatory-session-summary h3,
.observatory-evidence h3 {
font-size: var(--nodedc-font-size-lg);
letter-spacing: -0.02em;
line-height: 1.15;
}
.observatory-state p,
.observatory-evidence-empty p {
max-width: 52rem;
margin: 0.35rem 0 0;
color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.observatory-catalog-bar,
.observatory-session-summary {
align-self: start;
min-height: 0;
}
.observatory-catalog-bar__copy {
display: grid;
flex: 0 0 auto;
align-content: center;
gap: 0.1rem;
}
.observatory-catalog-bar h3 {
margin-top: 0.1rem;
}
.observatory-catalog-bar__controls {
min-width: 0;
flex: 1 1 auto;
flex-wrap: nowrap;
justify-content: flex-end;
}
.observatory-catalog-bar__controls .nodedc-select-anchor {
flex: 1 1 18rem;
min-width: 0;
}
.observatory-catalog-bar__controls > .nodedc-button {
flex: 0 0 auto;
}
.observatory-catalog-bar__run {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 0.55rem;
}
.observatory-state {
display: grid;
min-height: 19rem;
place-items: center;
align-content: center;
gap: 0.7rem;
text-align: center;
}
.observatory-state h3 {
max-width: 42rem;
margin: 0;
}
.observatory-session-stack {
display: grid;
grid-auto-rows: max-content;
align-content: start;
gap: 0.9rem;
container-name: observatory-session;
container-type: inline-size;
}
.observatory-session-summary {
display: grid;
grid-template-columns: minmax(11rem, 1.05fr) minmax(20rem, 1.6fr) minmax(11rem, auto);
align-items: center;
gap: 0.75rem;
}
.observatory-session-summary__identity {
display: grid;
min-width: 0;
gap: 0.22rem;
}
.observatory-session-summary__identity h3 {
margin: 0.1rem 0 0;
}
.observatory-session-summary__identity code {
overflow: hidden;
color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-xs);
text-overflow: ellipsis;
white-space: nowrap;
}
.observatory-session-summary__facts {
display: grid;
grid-template-columns: repeat(3, minmax(6.5rem, 1fr));
gap: 0.75rem;
margin: 0;
}
.observatory-session-summary__facts > div {
display: grid;
min-width: 0;
gap: 0.24rem;
}
.observatory-session-summary__facts dt {
color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-xs);
}
.observatory-session-summary__facts dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
color: var(--nodedc-text-secondary);
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.observatory-session-summary__end {
display: grid;
min-width: 0;
justify-items: end;
gap: 0.4rem;
}
.observatory-modalities {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.45rem;
align-items: center;
color: var(--nodedc-text-muted);
}
.observatory-evidence {
display: grid;
gap: 0.65rem;
}
.observatory-evidence > header {
padding: 0 0.2rem;
}
.observatory-evidence__ordering {
display: flex;
flex: none;
align-items: center;
gap: 0.55rem;
}
.observatory-evidence__sort {
display: grid;
width: 1.7rem;
height: 1.7rem;
place-items: center;
border: 0;
outline: 0;
border-radius: 0;
background: transparent;
padding: 0;
color: var(--nodedc-text-secondary);
cursor: pointer;
}
.observatory-evidence__sort:hover,
.observatory-evidence__sort:focus-visible {
background: var(--nodedc-glass-control-bg);
color: var(--nodedc-text-primary);
}
.observatory-evidence__sort svg {
width: 1.1rem;
height: 1.1rem;
transition: transform 160ms ease-out;
}
.observatory-evidence__sort path {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-width: 1.5;
}
.observatory-evidence__sort[data-direction="oldest"] svg {
transform: rotate(180deg);
}
.observatory-evidence-list {
display: grid;
gap: 0.4rem;
margin: 0;
padding: 0;
list-style: none;
}
.observatory-evidence-card {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center;
gap: 0.8rem;
min-height: 4.25rem;
padding: 0.7rem 0.85rem;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-glass-control-bg);
}
.observatory-evidence-card__icon {
display: grid;
width: 2.15rem;
height: 2.15rem;
place-items: center;
border-radius: var(--nodedc-radius-circle);
background: var(--nodedc-panel-icon-bg);
color: var(--nodedc-text-secondary);
}
.observatory-evidence-card__copy {
display: grid;
min-width: 0;
gap: 0.16rem;
}
.observatory-evidence-card__copy strong,
.observatory-evidence-card__copy span,
.observatory-evidence-card__copy small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.observatory-evidence-card__copy strong {
overflow: visible;
text-overflow: clip;
white-space: normal;
font-size: var(--nodedc-font-size-sm);
line-height: 1.3;
}
.observatory-evidence-card__copy span {
color: var(--nodedc-text-secondary);
font-size: var(--nodedc-font-size-sm);
}
.observatory-evidence-card__copy small {
color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-xs);
}
.observatory-evidence-card--progress {
grid-template-columns: auto minmax(0, 1fr) auto auto;
border: 0;
outline: 0;
}
.observatory-evidence-card__progress {
height: 0.28rem;
margin: 0.18rem 0 0.08rem;
overflow: hidden;
border-radius: 999px;
background: rgb(var(--nodedc-accent-rgb) / 0.12);
}
.observatory-evidence-card__progress > span {
display: block;
height: 100%;
border-radius: inherit;
background: rgb(var(--nodedc-accent-rgb));
transition: width 180ms ease-out;
}
.observatory-evidence-card__actions,
.observatory-replay__header,
.observatory-replay-state,
.observatory-replay-state__actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.85rem;
}
.observatory-replay-state {
flex-wrap: wrap;
}
.observatory-replay__header > div,
.observatory-replay-state > div {
min-width: 0;
}
.observatory-evidence-card__actions:empty {
display: none;
}
.observatory-replay,
.observatory-replay-state {
min-width: 0;
}
.observatory-replay {
display: grid;
gap: 0.85rem;
font-family: var(--nodedc-font-family);
}
.observatory-replay__header {
padding: 0.25rem 0.25rem 0;
}
.observatory-replay__header h3,
.observatory-replay-state h3 {
margin: 0.3rem 0 0;
}
.observatory-replay__header h3 {
font-size: var(--nodedc-font-size-sm);
line-height: 1.3;
}
.observatory-replay__header p,
.observatory-replay-state p,
.observatory-replay__description {
margin: 0.35rem 0 0;
color: var(--nodedc-text-muted);
font-family: var(--nodedc-font-family);
font-size: var(--nodedc-font-size-sm);
font-weight: var(--nodedc-font-weight-regular);
line-height: 1.35;
}
.observatory-replay__description {
margin-right: 0.25rem;
margin-left: 0.25rem;
}
.observatory-replay .m4-replay-threat-visual__buffering {
border: 0;
outline: 0;
}
.observatory-replay .laboratory-evidence-viewer {
border: 0;
outline: 0;
border-radius: 0;
box-shadow: none;
}
.observatory-evidence-empty {
display: grid;
min-height: 13rem;
place-items: center;
align-content: center;
gap: 0.55rem;
text-align: center;
}
.observatory-notice {
justify-content: flex-start;
color: var(--nodedc-text-secondary);
}
.observatory-notice__copy {
min-width: 0;
flex: 1 1 auto;
}
.observatory-rename-form,
.observatory-delete-confirmation {
display: grid;
gap: 0.75rem;
}
.observatory-delete-confirmation p,
.observatory-mutation-error {
margin: 0;
line-height: 1.5;
}
.observatory-mutation-error {
color: rgb(var(--nodedc-danger-rgb));
font-size: var(--nodedc-font-size-sm);
}
@container observatory-session (max-width: 46rem) {
.observatory-session-summary {
grid-template-columns: minmax(0, 1fr);
align-items: start;
}
.observatory-session-summary__end,
.observatory-modalities {
justify-items: start;
justify-content: flex-start;
}
}
@container observatory-session (max-width: 48rem) {
.observatory-evidence-card {
grid-template-columns: auto minmax(0, 1fr) auto;
}
.observatory-evidence-card > .nodedc-status {
display: none;
}
}
@container observatory-session (max-width: 38rem) {
.observatory-session-summary__facts {
grid-template-columns: minmax(0, 1fr);
}
.observatory-evidence-card__copy small {
white-space: normal;
}
}
@container observatory-workspace (max-width: 920px) {
.observatory-catalog-bar,
.observatory-catalog-bar__controls,
.observatory-notice {
align-items: stretch;
flex-direction: column;
}
.observatory-catalog-bar__controls {
flex-basis: auto;
}
.observatory-catalog-bar__controls .nodedc-select-anchor {
flex: 0 0 auto;
width: 100%;
min-width: 0;
}
.observatory-catalog-bar__run {
justify-content: flex-end;
}
}
@media (max-width: 920px) {
.observatory-replay__header,
.observatory-replay-state {
align-items: stretch;
flex-direction: column;
}
}
.observatory-ai-config {
display: grid;
gap: var(--nodedc-space-4);
}
.observatory-ai-config__lead,
.observatory-ai-config__error {
margin: 0;
color: var(--nodedc-text-secondary);
font-size: var(--nodedc-font-size-sm);
line-height: 1.45;
}
.observatory-ai-config__error {
color: rgb(var(--nodedc-danger-rgb));
}
.observatory-ai-config__duplicate {
margin-right: auto;
color: var(--nodedc-text-secondary);
font-size: var(--nodedc-font-size-sm);
line-height: 1.4;
}
@keyframes observatory-ai-calculate-pulse {
0%,
100% {
background-color: var(--nodedc-primary-action-bg);
}
50% {
background-color: color-mix(
in srgb,
var(--nodedc-primary-action-bg) 88%,
var(--nodedc-canvas-soft)
);
}
}
.observatory-ai-config__calculate--saving:disabled {
animation: observatory-ai-calculate-pulse 1.8s ease-in-out infinite;
cursor: progress;
opacity: 1;
}
.observatory-ai-config-window {
border: 0;
outline: 0;
box-shadow: var(--nodedc-shadow-window);
}
.observatory-ai-config-window::before,
.observatory-ai-config-window::after {
display: none;
}
.observatory-ai-module-group {
overflow: hidden;
border: 0;
outline: 0;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-glass-control-bg);
}
.observatory-ai-module-group > header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--nodedc-space-3);
padding: var(--nodedc-space-3) var(--nodedc-space-4);
border: 0;
outline: 0;
}
.observatory-ai-module-group > header > div {
display: grid;
min-width: 0;
gap: 0.2rem;
}
.observatory-ai-module-group > header .nodedc-status {
min-width: 0;
max-width: 55%;
overflow: hidden;
text-overflow: ellipsis;
}
.observatory-ai-module-group > header span {
color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-xs);
}
.observatory-ai-module-group__body {
padding: var(--nodedc-space-4);
border: 0;
outline: 0;
}
.observatory-ai-module-group__body .nodedc-select-anchor {
width: 100%;
}
.observatory-ai-config-window .nodedc-select-trigger,
.observatory-ai-config-window .nodedc-window__close {
border: 0;
outline: 0;
box-shadow: none;
}
.observatory-ai-config-window .nodedc-select-trigger:focus-visible,
.observatory-ai-config-window .nodedc-window__close:focus-visible {
background-color: var(--nodedc-focus-surface);
box-shadow:
var(--nodedc-glass-control-shadow),
inset 0 0 0 1px rgb(var(--nodedc-accent-rgb) / 0.22);
}
@media (max-width: 560px) {
.observatory-ai-config-window .nodedc-window__title {
white-space: normal;
}
.observatory-ai-module-group > header {
align-items: flex-start;
flex-direction: column;
}
.observatory-ai-module-group > header .nodedc-status {
max-width: 100%;
justify-content: flex-start;
}
}
@media (prefers-reduced-motion: reduce) {
.observatory-ai-config__calculate--saving:disabled {
animation: none;
background-color: color-mix(
in srgb,
var(--nodedc-primary-action-bg) 94%,
var(--nodedc-canvas-soft)
);
}
}
@@ -88,6 +88,13 @@
height: calc(100% + var(--rerun-native-chrome-height));
}
.rerun-viewport__runtime {
display: block;
width: 100%;
height: 100%;
border: 0;
}
.rerun-viewport__canvas canvas {
display: block;
width: 100% !important;
@@ -41,6 +41,7 @@ import { SimulationWorkspace } from "./simulation/SimulationWorkspace";
import { ComputeModulesWorkspace } from "./system/ComputeModulesWorkspace";
import { NetworkWorkspace } from "./system/NetworkWorkspace";
import { WorldMapWorkspace } from "./map/WorldMapWorkspace";
import { ObservatoryWorkspace } from "./observatory/ObservatoryWorkspace";
function statusTone(status: CapabilityStatus): "success" | "accent" | "warning" | "neutral" {
if (status === "active") return "success";
if (status === "ready") return "accent";
@@ -1191,6 +1192,8 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) {
);
case "simulations":
return <SimulationWorkspace />;
case "observatory":
return <ObservatoryWorkspace definition={props.definition} />;
case "device":
return null;
}
@@ -1,249 +1 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Icon, SegmentedControl } from "@nodedc/ui-react";
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
RerunViewport,
type RerunPlaybackController,
type RerunPlaybackState,
} from "../../components/RerunViewport";
import {
CanonicalRecordedLabReplay,
useCanonicalRecordedLabReplayState,
} from "../../components/laboratory/CanonicalRecordedLabReplay";
import {
resolveCanonicalLabReplay,
type CanonicalLabReplayDescriptor,
} from "../../core/laboratory/canonicalLabReplay";
import type { VegetationFullRouteReview } from "../../core/laboratory/vegetationShadow";
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
import { recordedSessionRerunProfile } from "../../core/observation/viewerProfile";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
import { defaultSceneSettings } from "../../sceneSettings";
type MediaMode = "video" | "camera";
type SpatialMode = "3d" | "plan";
type SpatialLayer = "source" | "local" | "tgs" | "semantic";
type SemanticLayer = "city" | "vegetation";
interface CanonicalReplayLaunch {
base: ObservationSessionReplayLaunch;
replay: CanonicalLabReplayDescriptor;
}
export function CanonicalVegetationRerunReplay({
resultId,
review,
}: {
resultId: string;
review: VegetationFullRouteReview;
}) {
const {
mediaMode,
spatialMode,
splitPrimarySize,
splitOrientation,
expanded,
onMediaModeChange,
onSpatialModeChange,
onSplitPrimarySizeChange,
onExpandedChange,
} = useCanonicalRecordedLabReplayState<MediaMode, SpatialMode>({
initialMediaMode: "video",
initialSpatialMode: "3d",
});
const [semanticLayer, setSemanticLayer] = useState<SemanticLayer>("vegetation");
const [showSemantics, setShowSemantics] = useState(true);
const [spatialLayer, setSpatialLayer] = useState<SpatialLayer>("source");
const [viewResetGeneration, setViewResetGeneration] = useState<0 | 1>(0);
const [playback, setPlayback] = useState<RerunPlaybackState | null>(null);
const [playbackController, setPlaybackController] =
useState<RerunPlaybackController | null>(null);
const [launch, setLaunch] = useState<CanonicalReplayLaunch | null>(null);
const [launchError, setLaunchError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
setLaunch(null);
setLaunchError(null);
void resolveObservationSessionReplay(review.sessionId, {
signal: controller.signal,
maximumWaitMs: 30 * 60 * 1000,
onUpdate: () => undefined,
}).then(async (value) => ({
base: value,
replay: await resolveCanonicalLabReplay(resultId, value, {
signal: controller.signal,
}),
})).then((value) => {
if (!controller.signal.aborted) setLaunch(value);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) {
setLaunchError(
caught instanceof Error ? caught.message : "Каноническая запись RAV004 недоступна.",
);
}
});
return () => controller.abort();
}, [resultId, review.sessionId]);
const splitView = mediaMode !== null && spatialMode !== null;
const sceneSettings = useMemo(() => ({
...defaultSceneSettings,
accumulationSeconds: spatialLayer === "local" ? 5 : 0,
showPoints: spatialMode !== null,
showTrajectory: spatialMode !== null,
showGrid: spatialMode !== null,
pointSize: 3.8,
}), [spatialLayer, spatialMode]);
const profile = launch ? recordedSessionRerunProfile({
sourceUrl: launch.replay.sourceUrl,
artifact: {
sourceUrl: launch.replay.sourceUrl,
viewerSourceUrl: launch.replay.viewerSourceUrl,
byteLength: launch.replay.byteLength,
sha256: launch.replay.sha256,
},
blueprintSourceUrl: launch.replay.blueprintSourceUrl,
autoplayWhenReady: false,
presentationGate: "ready",
expectedTimelineStartSeconds: launch.base.timelineStartSeconds,
expectedTimelineEndSeconds: launch.base.timelineEndSeconds,
initialPlaybackStartSeconds: review.timelineStartSeconds,
view: mediaMode !== null ? "perception" : "spatial",
viewResetGeneration,
followTrajectory: true,
semanticLayer,
unifiedPerception: splitView,
planView: spatialMode === "plan",
perceptionLayers: {
enabled: mediaMode !== null,
detections2d: mediaMode === "video",
segmentation: mediaMode === "video" && showSemantics,
cuboids3d: false,
},
perceptionRetryGeneration: 0,
lockPerceptionCameraInteraction: false,
}) : null;
const mediaLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Слои камеры и видео"
>
<Button
size="dense"
shape="pill"
variant={showSemantics ? "primary" : "secondary"}
aria-pressed={showSemantics}
onClick={() => setShowSemantics((visible) => !visible)}
>
СЕМАНТИКА
</Button>
<SegmentedControl
value={semanticLayer}
items={[
{ value: "city", label: "ГОРОД · EoMT" },
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
]}
label="Источник семантики"
size="dense"
onChange={(value) => {
setSemanticLayer(value);
setShowSemantics(true);
}}
/>
</div>
);
const spatialLayerControls = (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label="Пространственные слои RAV004"
>
<SegmentedControl
value={spatialLayer}
items={[
{ value: "source", label: "ИСХ. ТОЧКИ" },
{ value: "local", label: "ЛОК. SLAM" },
{ value: "tgs", label: "TGS", disabled: true },
{ value: "semantic", label: "СЕМАНТИКА", disabled: true },
]}
label="Пространственные слои"
size="dense"
onChange={setSpatialLayer}
/>
</div>
);
const resetSpatialView = (
<Button
size="dense"
variant="ghost"
icon={<Icon name="refresh" size={14} />}
aria-label="Сбросить положение 3D камеры"
title="Сбросить положение 3D камеры"
onClick={() => setViewResetGeneration((value) => value === 0 ? 1 : 0)}
>
</Button>
);
const transport = playback && playbackController ? (
<ObservationTimeline
className="m4-replay-threat-visual__timeline"
active
sourceCount={3}
mode="recorded"
seekable
synchronization="shared-clock"
rangeNs={playback.rangeNs}
currentNs={playback.currentNs}
playing={playback.playing}
onSeek={playbackController.seek}
onPlayingChange={playbackController.setPlaying}
showJumpToEnd={false}
/>
) : undefined;
return (
<CanonicalRecordedLabReplay
label="RAVNOVES004TREE · канонический повтор Rerun"
mediaMode={mediaMode ?? "none"}
mediaModes={[
{ value: "video", label: "ВИДЕО" },
{ value: "camera", label: "КАМЕРА" },
]}
spatialMode={spatialMode ?? "none"}
spatialModes={[
{ value: "3d", label: "3D" },
{ value: "plan", label: "ПЛАН" },
]}
expanded={expanded}
splitPrimarySize={splitPrimarySize}
splitOrientation={splitOrientation}
mediaAriaLabel={mediaMode === "camera" ? "Камера" : "Видео и семантика"}
spatialAriaLabel={spatialMode === "plan" ? "Вид сверху" : "Трёхмерная сцена"}
mediaLayerControls={mediaLayerControls}
spatialLayerControls={spatialLayerControls}
spatialLeadingControl={resetSpatialView}
mediaMultiLayer
unifiedContent={profile ? (
<RerunViewport
profile={profile}
sceneSettings={sceneSettings}
onPlaybackChange={setPlayback}
onPlaybackControllerChange={setPlaybackController}
/>
) : (
<div className="l3-visual-audit__state" role={launchError ? "alert" : "status"}>
{launchError ?? "Готовим единый кэш канонического повтора RAV004…"}
</div>
)}
emptyMessage="Выберите ВИДЕО/КАМЕРА или 3D/ПЛАН. Общие часы Rerun останутся на месте."
transport={transport}
onMediaModeChange={onMediaModeChange}
onSpatialModeChange={onSpatialModeChange}
onExpandedChange={onExpandedChange}
onSplitPrimarySizeChange={onSplitPrimarySizeChange}
/>
);
}
export { CanonicalVegetationRerunReplay } from "../../components/laboratory/CanonicalVegetationRerunReplay";
@@ -54,6 +54,7 @@ import {
buildLaboratoryProfiles,
experimentOptionsForProfile,
freshestLaboratorySelection,
isLegacyPublishedLaboratoryWork,
workOptionsForExperiment,
} from "./laboratoryArchiveProfiles";
import {
@@ -499,12 +500,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
onDeleteBegin: props.sessionArchive.onDeleteBegin,
});
const publishedWorks = useMemo(
() => sessions.items.filter((session) => (
session.lab !== null
&& session.status === "ready"
&& session.replayable
&& session.modalities.includes("point-cloud")
)).sort((left, right) => (
() => sessions.items.filter(isLegacyPublishedLaboratoryWork).sort((left, right) => (
Date.parse(right.lab?.runCreatedAtUtc ?? right.startedAtUtc)
- Date.parse(left.lab?.runCreatedAtUtc ?? left.startedAtUtc)
)),
@@ -459,6 +459,17 @@ function pipelineIdForSession(session: ObservationSessionSummary): string {
return session.lab?.resultKind ?? "legacy-perception";
}
/** Keep capability projections owned by Observatory out of the legacy LAB surface. */
export function isLegacyPublishedLaboratoryWork(
session: ObservationSessionSummary,
): boolean {
return session.lab !== null
&& session.lab.replayCapability === null
&& session.status === "ready"
&& session.replayable
&& session.modalities.includes("point-cloud");
}
export function buildLaboratoryCatalog({
rigLabel,
knownWorks,
File diff suppressed because it is too large Load Diff
@@ -17,9 +17,14 @@ import {
} from "../../core/system/useWorkerTelemetry";
function pipelineStateLabel(state: string): string {
if (state === "busy") return "Выполняет задачу";
if (state === "busy" || state === "running") return "Выполняет задачу";
if (state === "waiting") return "Ожидает восстановления данных";
if (state === "synchronizing") return "Синхронизирует поток";
if (state === "starting") return "Загружает профиль";
if (state === "stopped" || state === "cancelled") return "Профиль остановлен";
if (state === "failed") return "Ошибка профиля";
if (state === "ready") return "Готов к задаче";
return "Нет live-состояния";
return "Нет свежих данных о задаче";
}
function formatResourcePair(
@@ -116,7 +121,7 @@ export function ComputeModulesWorkspace() {
{!legacyDiagnostic && !agentTelemetry ? (
<GlassSurface className="system-workspace__notice" padding="md" tone="soft">
<StatusBadge tone="warning">
Агентный data-plane ещё не опубликовал нормализованный срез этого контура.
Ожидаем свежую телеметрию выбранного узла.
</StatusBadge>
</GlassSurface>
) : null}
@@ -187,6 +192,8 @@ export function ComputeModulesWorkspace() {
? `${node.gpu.temperature_celsius} °C`
: "—"
}</dd></div>
<div><dt>Частота GPU</dt><dd>{node?.gpu?.sm_clock_mhz == null ? "—" : `${node.gpu.sm_clock_mhz} МГц`}</dd></div>
<div><dt>Частота памяти GPU</dt><dd>{node?.gpu?.memory_clock_mhz == null ? "—" : `${node.gpu.memory_clock_mhz} МГц`}</dd></div>
<div><dt>Мощность</dt><dd>{
typeof node?.gpu?.power_watts === "number"
? `${node.gpu.power_watts.toFixed(1)} Вт`
@@ -214,7 +221,7 @@ export function ComputeModulesWorkspace() {
<div>
<span className="section-eyebrow">PROCESSING RUNTIME</span>
<h3>Контейнеры Mission Core</h3>
<p>Два ограниченных runtime: inference server и прикладной perception pipeline.</p>
<p>Состояние контейнера и готовность вычислительного профиля проверяются отдельно.</p>
</div>
<StatusBadge tone={node?.triton.ready ? "success" : "danger"}>
{node?.triton.ready ? "Triton ready" : "Triton недоступен"}
@@ -233,19 +240,31 @@ export function ComputeModulesWorkspace() {
<span className="section-eyebrow">ТЕКУЩАЯ ЗАДАЧА</span>
<h3>{pipelineStateLabel(telemetry?.pipeline.service_state ?? "unavailable")}</h3>
<p>
{telemetry?.pipeline.profile_name ?? "Perception"}
{" · "}
{telemetry?.pipeline.active_request_id
? `Run ${telemetry.pipeline.active_request_id}`
: "Очередь свободна; модели остаются загруженными в persistent worker."}
: telemetry?.pipeline.service_state === "ready"
? "Готов к приёму данных."
: "Готовность моделей не подтверждена."}
</p>
</div>
<StatusBadge tone={
telemetry?.pipeline.service_state === "busy" ? "warning"
["busy", "running", "waiting", "synchronizing", "starting"].includes(telemetry?.pipeline.service_state ?? "") ? "warning"
: telemetry?.pipeline.service_state === "ready" ? "success"
: "danger"
}>
{telemetry?.pipeline.service_state ?? "unavailable"}
{pipelineStateLabel(telemetry?.pipeline.service_state ?? "unavailable")}
</StatusBadge>
</header>
{telemetry?.pipeline.profile_name ? (
<div className="worker-pipeline__summary">
<div><span>Процессы профиля</span><strong>{telemetry.pipeline.live_children ?? "—"}</strong></div>
<div><span>Ожиданий потока</span><strong>{telemetry.pipeline.input_pauses ?? "—"}</strong></div>
<div><span>Пакетов в очереди</span><strong>{telemetry.pipeline.pending_bundles ?? "—"}</strong></div>
<div><span>Входные буферы</span><strong>{formatBytes(telemetry.pipeline.buffer_bytes)}</strong></div>
</div>
) : null}
<div className="worker-pipeline__summary">
<div><span>Завершено запусков</span><strong>{telemetry?.pipeline.completed_runs ?? "—"}</strong></div>
<div><span>Ошибок запусков</span><strong>{telemetry?.pipeline.failed_runs ?? "—"}</strong></div>
@@ -11,6 +11,7 @@ let fetchM49TgsAnchorSpatial;
let AdvancedLaboratoryContractError;
let buildLaboratoryCatalog;
let buildLaboratoryProfiles;
let isLegacyPublishedLaboratoryWork;
let experimentOptionsForProfile;
let freshestLaboratorySelection;
let workOptionsForExperiment;
@@ -928,6 +929,7 @@ before(async () => {
({
buildLaboratoryCatalog,
buildLaboratoryProfiles,
isLegacyPublishedLaboratoryWork,
experimentOptionsForProfile,
freshestLaboratorySelection,
workOptionsForExperiment,
@@ -962,6 +964,31 @@ after(async () => {
await server?.close();
});
test("capability-owned projections never enter the legacy LAB catalog", () => {
const legacy = {
status: "ready",
replayable: true,
modalities: ["point-cloud", "trajectory"],
lab: { replayCapability: null },
};
const capabilityProjection = {
...legacy,
lab: {
replayCapability: {
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
kind: "canonical-recorded-rerun",
viewerProfile: "recorded-session",
timeline: "session_time",
activation: "explicit",
commandsEnabled: false,
},
},
};
assert.equal(isLegacyPublishedLaboratoryWork(legacy), true);
assert.equal(isLegacyPublishedLaboratoryWork(capabilityProjection), false);
});
test("LAB catalog is pipeline-scoped and ordered by real run time", () => {
const catalog = buildLaboratoryCatalog({
rigLabel: "K1",
@@ -104,3 +104,53 @@ test("laboratory UI is a bounded feature slice, not a central workspace branch",
assert.doesNotMatch(laboratoryCss, /\.e30-human-review/);
assert.match(e30HumanReviewCss, /\.e30-human-review/);
});
test("Observatory owns a bounded explicit-open lifecycle around the shared recorded viewer", async () => {
const workspaceHub = await read("workspaces/Workspaces.tsx");
const observatory = await read("workspaces/observatory/ObservatoryWorkspace.tsx");
const observatoryCore = await read("core/observatory/catalog.ts");
const recordedRun = await read("core/observatory/recordedRun.ts");
const sharedReplay = await read(
"components/laboratory/CanonicalResultRerunReplay.tsx",
);
const workspaceCss = await read("styles/workspaces.css");
const observatoryCss = await read("styles/observatory.css");
assert.match(
workspaceHub,
/case "observatory":[\s\S]*<ObservatoryWorkspace definition=\{props\.definition\} \/>/,
);
assert.match(observatory, /export function ObservatoryWorkspace/);
assert.match(observatory, /useObservatoryCatalog/);
assert.match(observatory, /data-observatory-authority="observation-only"/);
assert.match(
observatory,
/data-observatory-viewer={replay\.kind === "ready" \|\| replay\.kind === "composition" \? "attached" : "detached"}/,
);
assert.doesNotMatch(
`${observatory}\n${observatoryCore}`,
/(?:core|workspaces)\/laboratory|\/api\/v1\/laboratory|RerunViewport|ObservationSessionSelect/,
);
assert.match(observatory, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/);
assert.match(observatory, /closeReplay\(\);[\s\S]*setSelectedSessionId/);
assert.doesNotMatch(
observatory,
/replayObservationSession|deleteObservationSession|useObservationSessions|useAdvancedLaboratoryCatalog/,
);
assert.match(recordedRun, /fetchVegetationShadowResultMetadata/);
assert.doesNotMatch(
recordedRun,
/advanced-index|resolveObservationSessionReplay|resolveCanonicalLabReplay|RerunViewport/,
);
assert.equal(sharedReplay.match(/<RerunViewport\b/g)?.length, 1);
assert.match(sharedReplay, /recordedSessionRerunProfile/);
assert.match(sharedReplay, /useState<0 \| 1>\(hasTgs \? 1 : 0\)/);
assert.match(sharedReplay, /hasTgs \? 0\.000001 : 0/);
for (const adapter of ["CanonicalVegetationRerunReplay", "PortableResultReplay"]) {
const source = await read(`components/laboratory/${adapter}.tsx`);
assert.match(source, /<CanonicalResultRerunReplay/);
assert.doesNotMatch(source, /<RerunViewport|<video|setInterval|Canvas|THREE\./);
}
assert.doesNotMatch(workspaceCss, /\.observatory-/);
assert.match(observatoryCss, /\.observatory-workspace/);
});
@@ -132,3 +132,32 @@ test("canonical LAB resolves one generation-bound merged RRD", async () => {
blueprintSourceUrl: "/api/v1/observation-sessions/session-001/blueprint.rrd",
});
});
for (const [prefix, sourceKind] of [
["m49-tgs-portable-review", "portable-tgs"],
["lab-v1-eomt-ddrnet", "portable-semantic"],
]) test(`${sourceKind} resolves Core cache for the exact result and base, never a legacy LAB`, async () => {
const base = "a".repeat(64), generation = "b".repeat(64);
const resultId = `${prefix}-${"c".repeat(64)}`;
const launch = { sourceUrl: "/api/v1/observation-sessions/source/recording.rrd",
viewerSourceUrl: `/api/v1/observation-sessions/source/recording.rrd?generation=${base}`,
sha256: base };
let calls = 0;
const fetcher = async (url, options) => {
calls++;
assert.equal(url, `http://mission-core.test/api/v1/observatory/portable-results/${resultId}/replays/${base}/recording.rrd`);
assert.equal(options.method, "HEAD");
return new Response(null, { headers: { "Content-Type": "application/vnd.rerun.rrd",
"Content-Length": "100", "ETag": `"${generation}"`, "X-Rerun-Format": "RRF2" } });
};
const options = { sourceKind, origin: "http://mission-core.test", fetcher };
const replay = await resolveCanonicalLabReplay(resultId, launch, options);
assert.equal(replay.viewerSourceUrl, `${replay.sourceUrl}?generation=${generation}`);
assert.equal(replay.blueprintSourceUrl, "/api/v1/observation-sessions/source/blueprint.rrd");
await assert.rejects(resolveCanonicalLabReplay(`lab-v1-vegetation-shadow-${"c".repeat(64)}`, launch, options));
await assert.rejects(resolveCanonicalLabReplay(resultId, launch, {
...options, sourceKind: sourceKind === "portable-tgs" ? "portable-semantic" : "portable-tgs",
}));
await assert.rejects(resolveCanonicalLabReplay(resultId, { ...launch, sha256: "d".repeat(64) }, options));
assert.equal(calls, 1);
});
@@ -0,0 +1,97 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server, fetchLabViewProfile, saveLabViewProfile, normalizeLabSceneSettings;
before(async () => {
server = await createServer({ appType: "custom", logLevel: "silent",
server: { middlewareMode: true } });
({ fetchLabViewProfile, saveLabViewProfile } = await server.ssrLoadModule(
"/src/core/observatory/labViewProfile.ts"));
({ normalizeLabSceneSettings } = await server.ssrLoadModule(
"/src/components/laboratory/CanonicalResultRerunReplay.tsx"));
});
after(async () => { await server?.close(); });
const serverProfile = {
schema_version: "missioncore.observatory-lab-view-profile/v1",
result_id: "result-1",
scene_settings: {
point_size: 4.7,
accumulation_seconds: 8,
color_mode: "height",
palette: "viridis",
show_grid: false,
show_labels: true,
show_camera_frustums: false,
},
updated_at_utc: "2026-09-04T09:30:00.000Z",
};
test("LAB view profile round-trips through the result-scoped server endpoint", async () => {
const originalFetch = globalThis.fetch;
const requests = [];
globalThis.fetch = async (url, init = {}) => {
requests.push({ url, init });
return new Response(JSON.stringify(serverProfile), {
status: 200, headers: { "Content-Type": "application/json" },
});
};
try {
const settings = await fetchLabViewProfile("result-1");
assert.equal(settings.pointSize, 4.7);
assert.equal(settings.palette, "viridis");
const saved = await saveLabViewProfile("result-1", settings);
assert.equal(saved.accumulationSeconds, 8);
assert.equal(requests[0].url, "/api/v1/observatory/lab-view-profiles/result-1");
assert.equal(requests[1].init.method, "PUT");
assert.deepEqual(JSON.parse(requests[1].init.body).scene_settings, serverProfile.scene_settings);
} finally {
globalThis.fetch = originalFetch;
}
});
test("LAB view profile fails closed on a foreign result identity", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response(JSON.stringify({
...serverProfile, result_id: "foreign-result",
}), { status: 200, headers: { "Content-Type": "application/json" } });
try {
await assert.rejects(fetchLabViewProfile("result-1"), /другой LAB/);
} finally {
globalThis.fetch = originalFetch;
}
});
test("LAB scene settings normalize typed values before persistence", () => {
assert.deepEqual(
normalizeLabSceneSettings({
pointSize: 28,
accumulationSeconds: 305,
colorMode: "height",
palette: "viridis",
customColor: "#ffffff",
showGrid: false,
showPoints: true,
showTrajectory: true,
showLabels: true,
showCameraFrustums: false,
projection: "3d",
}),
{
pointSize: 28,
accumulationSeconds: 305,
colorMode: "height",
palette: "viridis",
customColor: "#ffffff",
showGrid: false,
showPoints: true,
showTrajectory: true,
showLabels: true,
showCameraFrustums: false,
projection: "3d",
},
);
});
@@ -887,7 +887,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(canonical, /secondary=\{spatialPane/);
assert.match(canonical, /primarySize=\{splitView \? splitPrimarySize : mediaMode !== "none" \? 100 : 0\}/);
assert.match(canonical, /resizable=\{splitView\}/);
assert.match(canonical, /separatorLabel="Изменить размер VIDEO\/CAMERA и 3D\/PLAN"/);
assert.match(canonical, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
assert.match(canonical, /secondaryMode=\{\{/);
assert.match(visual, /playback=\{playbackController\.playback\}/);
assert.match(visual, /clock: "external"/);
@@ -77,6 +77,70 @@ function session(overrides = {}) {
};
}
function canonicalLab(overrides = {}) {
const resultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
return {
lab_id: "LAB V1",
source_session_id: "20260828T130511Z_viewer_live",
result_kind: "recorded-perception-qualification",
result_id: resultId,
source_result_id: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
config_sha256: null,
run_created_at_utc: "2026-08-29T18:05:11.329061+00:00",
published_at_utc: "2026-08-30T12:00:00.000Z",
replay_capability: {
schema_version: "missioncore.observation-lab-replay-capability/v1",
kind: "canonical-recorded-rerun",
viewer_profile: "recorded-session",
timeline: "session_time",
activation: "explicit",
commands_enabled: false,
},
provenance: {},
...overrides,
};
}
function legacyCalculationProfile(overrides = {}) {
return {
schema_version: "missioncore.observatory-calculation-profile/v1",
setup_id: "lab-v1-ravnoves004tree-final",
display_name: "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39",
origin: "existing-result",
definition_id: null,
definition_version: null,
definition_sha256: null,
...overrides,
};
}
function canonicalProjectionProvenance(resultId, replayCapability) {
return {
schema_version: "missioncore.canonical-recorded-lab-projection/v1",
evidence_identity_sha256: resultId.slice("lab-v1-vegetation-shadow-".length),
result_document_sha256: "a".repeat(64),
replay_capability: replayCapability,
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
actuation_accepted: false,
},
method: {
schema_version: "missioncore.laboratory-method/v1",
completeness: "legacy-partial",
execution_class: "ai-inference",
pipeline_id: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
components: [{
kind: "source",
name: "sealed full-route LAB result",
version: "missioncore.lab-v1-vegetation-shadow/v1",
role: "immutable Session catalog projection",
identity_sha256: resultId.slice("lab-v1-vegetation-shadow-".length),
}],
},
};
}
function replay(overrides = {}) {
const sessionId = overrides.session_id ?? "session-20260716T205632Z";
const sourceUrl = overrides.source_url ??
@@ -134,6 +198,7 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo
modalities: ["point-cloud", "pose"],
durationSeconds: 1_450.744,
replayable: true,
captureAttestation: null,
preparation: null,
lab: null,
}]);
@@ -141,6 +206,137 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo
assert.equal("path" in catalog.items[0], false);
});
test("session catalog strictly decodes the explicit recorded LAB replay capability", () => {
const resultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
const decoded = decodeObservationSessionCatalog({
items: [session({ id: resultId, lab: canonicalLab() })],
}).items[0].lab;
assert.deepEqual(decoded.replayCapability, {
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
kind: "canonical-recorded-rerun",
viewerProfile: "recorded-session",
timeline: "session_time",
activation: "explicit",
commandsEnabled: false,
});
const rollingUpgradeLab = canonicalLab();
rollingUpgradeLab.provenance = canonicalProjectionProvenance(
resultId,
rollingUpgradeLab.replay_capability,
);
delete rollingUpgradeLab.replay_capability;
assert.deepEqual(
decodeObservationSessionCatalog({
items: [session({ id: resultId, lab: rollingUpgradeLab })],
}).items[0].lab.replayCapability,
decoded.replayCapability,
);
const invalidComponents = [
{ kind: "tool" },
{ name: "unsealed result" },
{ version: "missioncore.lab-v1-vegetation-shadow/v0" },
{ role: "mutable projection" },
{ identity_sha256: "0".repeat(64) },
{ unexpected: true },
];
for (const componentOverride of invalidComponents) {
const invalidRollingUpgradeLab = canonicalLab();
invalidRollingUpgradeLab.provenance = canonicalProjectionProvenance(
resultId,
invalidRollingUpgradeLab.replay_capability,
);
invalidRollingUpgradeLab.provenance.method.components = [{
...invalidRollingUpgradeLab.provenance.method.components[0],
...componentOverride,
}];
delete invalidRollingUpgradeLab.replay_capability;
assert.throws(
() => decodeObservationSessionCatalog({
items: [session({ id: resultId, lab: invalidRollingUpgradeLab })],
}),
ObservationSessionContractError,
);
}
assert.throws(
() => decodeObservationSessionCatalog({
items: [session({
id: resultId,
lab: canonicalLab({
replay_capability: {
...canonicalLab().replay_capability,
commands_enabled: true,
},
}),
})],
}),
ObservationSessionContractError,
);
});
test("session catalog decodes a typed calculation profile without inferring unknown results", () => {
const resultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
const decoded = decodeObservationSessionCatalog({
items: [session({
id: resultId,
lab: canonicalLab({ calculation_profile: legacyCalculationProfile() }),
})],
}).items[0].lab;
assert.deepEqual(decoded.calculationProfile, {
schemaVersion: "missioncore.observatory-calculation-profile/v1",
setupId: "lab-v1-ravnoves004tree-final",
displayName: "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39",
origin: "existing-result",
definitionId: null,
definitionVersion: null,
definitionSha256: null,
});
assert.equal(
decodeObservationSessionCatalog({
items: [session({ id: resultId, lab: canonicalLab() })],
}).items[0].lab.calculationProfile,
null,
);
assert.equal(
decodeObservationSessionCatalog({
items: [session({
id: resultId,
lab: canonicalLab({ calculation_profile: null }),
})],
}).items[0].lab.calculationProfile,
null,
);
assert.throws(
() => decodeObservationSessionCatalog({
items: [session({
id: resultId,
lab: canonicalLab({
calculation_profile: legacyCalculationProfile({
origin: "archived-definition",
definition_id: "lab-v1-portable",
}),
}),
})],
}),
ObservationSessionContractError,
);
assert.throws(
() => decodeObservationSessionCatalog({
items: [session({
id: resultId,
lab: canonicalLab({
calculation_profile: {
...legacyCalculationProfile(),
guessed_from_provenance: true,
},
}),
})],
}),
ObservationSessionContractError,
);
});
test("opened archive is named in the scene header and trash hover has no pill", async () => {
const appSource = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
const styles = await readFile(
@@ -246,7 +442,7 @@ test("source and laboratory catalogs are requested as disjoint backend projectio
assert.deepEqual(calls, [
"/api/v1/observation-sessions?limit=100&scope=source",
"/api/v1/observation-sessions?limit=100&scope=laboratory",
"/api/v1/observation-sessions?limit=100&scope=laboratory&lab_contract=v3",
]);
});
@@ -40,6 +40,7 @@ let formatAccumulationDuration;
let resolveRerunSourceUrl;
let resolveRecordedBlueprintUrl;
let fetchRecordedBlueprintRrd;
let recordedCameraJournalContract;
let resolveRecordedPerceptionUrl;
let fetchRecordedPerceptionRrd;
let resolveRecordedPerceptionViewerSourceUrl;
@@ -110,6 +111,7 @@ before(async () => {
resolveRerunSourceUrl,
resolveRecordedBlueprintUrl,
fetchRecordedBlueprintRrd,
recordedCameraJournalContract,
resolveRecordedPerceptionUrl,
fetchRecordedPerceptionRrd,
resolveRecordedPerceptionViewerSourceUrl,
@@ -118,7 +120,7 @@ before(async () => {
fetchRecordedPointColorsRrd,
recordedPointColorKey,
isRecordedPlaybackFullyBuffered,
} = await server.ssrLoadModule(
} = await server.ssrLoadModule(
"/src/components/RerunViewport.tsx",
));
({ recordedObservationSources } = await server.ssrLoadModule(
@@ -129,6 +131,25 @@ before(async () => {
));
});
test("follow toggles retain the current recorded camera journal", () => {
const before = recordedCameraJournalContract({
activeView: "spatial", viewResetGeneration: 0, planView: false, followTrajectory: false,
});
const afterFollowToggle = recordedCameraJournalContract({
activeView: "spatial", viewResetGeneration: 0, planView: false, followTrajectory: true,
});
const afterPlanToggle = recordedCameraJournalContract({
activeView: "spatial", viewResetGeneration: 0, planView: true, followTrajectory: true,
});
const afterReset = recordedCameraJournalContract({
activeView: "spatial", viewResetGeneration: 1, planView: false, followTrajectory: false,
});
assert.equal(afterFollowToggle, before);
assert.notEqual(afterPlanToggle, before);
assert.notEqual(afterReset, before);
});
test("observation camera windows tile from the bottom-right above the live timeline", () => {
const bounds = { width: 1280, height: 720 };
const left = initialObservationWindowRect(0, 2, bounds);
@@ -444,6 +465,7 @@ test("recorded replay becomes ready only after the complete declared timeline is
test("recorded blueprint fetch is bounded, strict and sends only display settings", async () => {
const calls = [];
const cameraLimits = [];
const payload = Uint8Array.from([0x52, 0x52, 0x46, 0x32, 0x01]);
const result = await fetchRecordedBlueprintRrd(
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
@@ -464,6 +486,15 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
activeView: "perception3d",
viewResetGeneration: 1,
followTrajectory: true,
cameraEye: {
position: [3, 4, 5],
lookTarget: [1, 2, 0],
eyeUp: [0, 0, 1],
},
eyeRelativeToTracking: true,
currentTimeNs: 39_215_263_458,
onCameraMaxOrbitalRadius: value => cameraLimits.push(value),
unifiedCameraShare: 0.73,
perceptionLayers: {
enabled: true,
detections2d: true,
@@ -474,7 +505,10 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
calls.push({ input: String(input), init, body: JSON.parse(String(init.body)) });
return new Response(payload, {
status: 200,
headers: { "Content-Type": "application/vnd.rerun.rrd" },
headers: {
"Content-Type": "application/vnd.rerun.rrd",
"X-MissionCore-Camera-Max-Orbital-Radius": "686.024231",
},
});
},
},
@@ -497,13 +531,21 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
active_view: "perception3d",
view_reset_generation: 1,
follow_trajectory: true,
current_time_ns: 39_215_263_458,
unified_perception: true,
unified_camera_share: 0.73,
semantic_layer: null,
plan_view: false,
eye_position: [3, 4, 5],
eye_look_target: [1, 2, 0],
eye_up: [0, 0, 1],
eye_relative_to_tracking: true,
show_camera_image: true,
show_detections_2d: true,
show_segmentation: false,
show_cuboids_3d: true,
});
assert.deepEqual(cameraLimits, [686.024231]);
await fetchRecordedBlueprintRrd(
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
@@ -537,6 +579,7 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
},
);
assert.equal(calls[1].body.unified_perception, false);
assert.equal(calls[1].body.unified_camera_share, 0.46);
assert.equal(calls[1].body.show_cuboids_3d, true);
await assert.rejects(
@@ -0,0 +1,518 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let applyObservatoryCatalogMutationOverlay;
let buildObservatoryCatalog;
let fetchObservatoryCatalog;
let findObservatoryEvidence;
let observatoryCatalogConfirmsEvidenceDeletion;
let reconcileObservatoryCatalogMutationOverlay;
let ObservatoryCatalogContractError;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
applyObservatoryCatalogMutationOverlay,
buildObservatoryCatalog,
fetchObservatoryCatalog,
findObservatoryEvidence,
observatoryCatalogConfirmsEvidenceDeletion,
reconcileObservatoryCatalogMutationOverlay,
ObservatoryCatalogContractError,
} = await server.ssrLoadModule("/src/core/observatory/catalog.ts"));
});
after(async () => {
await server?.close();
});
function source(id, startedAtUtc) {
return {
id,
label: `Источник ${id}`,
startedAtUtc,
completedAtUtc: startedAtUtc,
status: "ready",
modalities: ["point-cloud", "video"],
durationSeconds: 42,
replayable: true,
preparation: null,
lab: null,
};
}
function evidence(id, sourceSessionId, publishedAtUtc) {
return {
id,
label: `Результат ${id}`,
startedAtUtc: publishedAtUtc,
completedAtUtc: publishedAtUtc,
status: "ready",
modalities: ["video"],
durationSeconds: 12,
replayable: true,
preparation: null,
lab: {
labId: `LAB-${id}`,
sourceSessionId,
resultKind: "recorded-evidence",
resultId: id,
sourceResultId: null,
configSha256: "a".repeat(64),
runCreatedAtUtc: publishedAtUtc,
publishedAtUtc,
replayCapability: {
schemaVersion: "missioncore.observation-lab-replay-capability/v2",
kind: "portable-result-review", viewerProfile: "portable-result",
timeline: "result-defined", activation: "explicit", commandsEnabled: false,
},
calculationProfile: null,
provenance: {
schema_version: "missioncore.observatory-portable-result-publication/v1",
authority: { commands_enabled: false },
calculation_profile: null, calculation_profile_sha256: "1".repeat(64),
job: {}, method: {}, storage: {},
source: { session_id: sourceSessionId },
run_definition: { definition_sha256: "a".repeat(64) },
result_package: { manifest_sha256: "b".repeat(64), artifact_manifest_id: "c".repeat(64) },
replay_capability: {
schema_version: "missioncore.observation-lab-replay-capability/v2",
kind: "portable-result-review", viewer_profile: "portable-result",
timeline: "result-defined", activation: "explicit", commands_enabled: false,
},
},
},
};
}
function canonicalProvenance(resultId) {
const identitySha256 = resultId.slice("lab-v1-vegetation-shadow-".length);
return {
schema_version: "missioncore.canonical-recorded-lab-projection/v1",
evidence_identity_sha256: identitySha256,
result_document_sha256: "a".repeat(64),
replay_capability: {
schema_version: "missioncore.observation-lab-replay-capability/v1",
kind: "canonical-recorded-rerun",
viewer_profile: "recorded-session",
timeline: "session_time",
activation: "explicit",
commands_enabled: false,
},
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
actuation_accepted: false,
},
method: {
schema_version: "missioncore.laboratory-method/v1",
completeness: "legacy-partial",
execution_class: "ai-inference",
pipeline_id: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
components: [{
kind: "source",
name: "sealed full-route LAB result",
version: "missioncore.lab-v1-vegetation-shadow/v1",
role: "immutable Session catalog projection",
identity_sha256: identitySha256,
}],
},
};
}
test("Observatory joins evidence only by sourceSessionId and keeps deterministic order", () => {
const catalog = buildObservatoryCatalog(
[
source("older", "2026-08-28T10:00:00Z"),
source("newer", "2026-08-29T10:00:00Z"),
],
[
evidence("old-result", "newer", "2026-08-29T11:00:00Z"),
evidence("new-result", "newer", "2026-08-29T12:00:00Z"),
evidence("orphan", "missing", "2026-08-29T13:00:00Z"),
],
);
assert.deepEqual(catalog.items.map(({ source: item }) => item.id), ["newer", "older"]);
assert.deepEqual(
catalog.items[0].evidence.map((item) => item.sessionId),
["new-result", "old-result"],
);
assert.deepEqual(catalog.unresolvedEvidence.map((item) => item.sessionId), ["orphan"]);
assert.deepEqual(catalog.window, {
limit: 100,
sourceCount: 2,
laboratoryCount: 3,
sourceLimitReached: false,
laboratoryLimitReached: false,
});
assert.equal("verdict" in catalog.items[0].evidence[0], false);
assert.equal(catalog.items[1].evidence.length, 0);
});
test("Observatory fails visibly when the laboratory projection loses its typed link", () => {
assert.throws(
() => buildObservatoryCatalog(
[source("source", "2026-08-29T10:00:00Z")],
[source("not-lab", "2026-08-29T11:00:00Z")],
),
ObservatoryCatalogContractError,
);
});
test("Observatory fetches disjoint read-only source and laboratory projections", async () => {
const calls = [];
const fetcher = async (input, init) => {
calls.push({ input: String(input), method: init?.method });
return new Response(JSON.stringify({
schema_version: "missioncore.observation-session-page/v1", items: [], next_cursor: null,
}), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const catalog = await fetchObservatoryCatalog({ fetcher, limit: 50 });
assert.deepEqual(catalog, {
items: [],
unresolvedEvidence: [],
window: {
limit: 50,
sourceCount: 0,
laboratoryCount: 0,
sourceLimitReached: false,
laboratoryLimitReached: false,
},
});
assert.deepEqual(
calls.map(({ input }) => input).sort(),
[
"/api/v1/observation-sessions?limit=50&scope=laboratory&lab_contract=v3&pagination=cursor-v1",
"/api/v1/observation-sessions?limit=50&scope=source&pagination=cursor-v1",
],
);
assert.deepEqual(new Set(calls.map(({ method }) => method)), new Set(["GET"]));
});
test("Observatory exposes bounded-window uncertainty without inventing a broken link", () => {
const catalog = buildObservatoryCatalog(
[
source("newer", "2026-08-29T10:00:00Z"),
source("older", "2026-08-28T10:00:00Z"),
],
[
evidence("linked", "newer", "2026-08-29T11:00:00Z"),
evidence("outside-window", "older-than-window", "2026-08-29T12:00:00Z"),
],
2,
);
assert.equal(catalog.window.sourceLimitReached, true);
assert.equal(catalog.window.laboratoryLimitReached, true);
assert.deepEqual(
catalog.unresolvedEvidence.map((item) => item.sessionId),
["outside-window"],
);
});
test("historical canonical replay stays out of Observatory without modifying its archive record", () => {
const canonicalResultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
const canonical = evidence(
canonicalResultId,
"20260828T130511Z_viewer_live",
"2026-08-29T18:05:11Z",
);
canonical.lab = {
...canonical.lab,
labId: "LAB V1",
resultKind: "recorded-perception-qualification",
resultId: canonicalResultId,
sourceResultId: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
configSha256: null,
provenance: canonicalProvenance(canonicalResultId),
replayCapability: {
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
kind: "canonical-recorded-rerun",
viewerProfile: "recorded-session",
timeline: "session_time",
activation: "explicit",
commandsEnabled: false,
},
};
const before = structuredClone(canonical);
const catalog = buildObservatoryCatalog(
[source("20260828T130511Z_viewer_live", "2026-08-28T13:05:11Z")],
[canonical],
);
assert.deepEqual(catalog.items[0].evidence, []);
assert.equal(catalog.window.laboratoryCount, 0);
assert.deepEqual(canonical, before);
});
test("only admitted portable evidence is visible regardless of LAB label, age or installed version", () => {
const legacy = evidence("legacy", "source", "2026-09-03T13:00:00Z");
legacy.lab.labId = "LAB M4.9T5";
legacy.lab.replayCapability = null;
const historical = evidence("old-portable", "source", "2026-07-20T10:00:00Z");
historical.lab.labId = "LAB E24"; // Labels must never become an allow/deny list.
const current = evidence("new-portable", "source", "2026-09-03T12:00:00Z");
const catalog = buildObservatoryCatalog(
[source("source", "2026-07-19T10:00:00Z")], [legacy, historical, current],
);
assert.deepEqual(catalog.items[0].evidence.map((item) => item.sessionId), ["new-portable", "old-portable"]);
assert.equal(catalog.window.laboratoryCount, 2);
assert.equal(findObservatoryEvidence(catalog, "legacy"), null);
});
test("a malformed portable binding errors instead of silently disappearing as legacy", () => {
const broken = evidence("broken", "source", "2026-09-03T12:00:00Z");
broken.lab.provenance.source.session_id = "another-source";
assert.throws(() => buildObservatoryCatalog(
[source("source", "2026-07-19T10:00:00Z")], [broken],
), /identity/);
});
function wire(item) {
const lab = item.lab;
return {
id: item.id, label: item.label, started_at_utc: item.startedAtUtc,
completed_at_utc: item.completedAtUtc, status: item.status,
modalities: item.modalities, duration_seconds: item.durationSeconds, replayable: item.replayable,
...(lab ? { lab: {
lab_id: "LAB M4.9T5", source_session_id: lab.sourceSessionId, result_kind: lab.resultKind,
result_id: lab.resultId, source_result_id: lab.sourceResultId, config_sha256: lab.configSha256,
run_created_at_utc: lab.runCreatedAtUtc, published_at_utc: lab.publishedAtUtc,
provenance: lab.provenance, replay_capability: lab.replayCapability ? lab.provenance.replay_capability : null,
calculation_profile: null,
} } : {}),
};
}
function pageResponse(items, nextCursor = null) {
return new Response(JSON.stringify({
schema_version: "missioncore.observation-session-page/v1",
items: items.map(wire), next_cursor: nextCursor,
}), { headers: { "Content-Type": "application/json" } });
}
test("500 sources and later-page results are searchable metadata, with no archive or viewer requests", async () => {
const sources = Array.from({ length: 500 }, (_, index) => source(`source-${index}`, "2026-08-29T10:00:00Z"));
const labs = Array.from({ length: 101 }, (_, index) => {
const item = evidence(`legacy-${index}`, sources[0].id, "2026-08-29T11:00:00Z");
item.lab.replayCapability = null;
return item;
});
labs.push(evidence("portable-after-legacy", sources[499].id, "2026-08-29T12:00:00Z"));
const calls = [];
const catalog = await fetchObservatoryCatalog({ fetcher: async (input, init) => {
const url = new URL(String(input), "http://localhost");
calls.push(url);
assert.equal(init.method, "GET");
assert.equal(url.pathname, "/api/v1/observation-sessions");
const rows = url.searchParams.get("scope") === "source" ? sources : labs;
const cursor = url.searchParams.get("cursor");
const start = cursor === null ? 0 : rows.findIndex((item) => item.id === cursor) + 1;
const page = rows.slice(start, start + 100);
return pageResponse(page, start + page.length < rows.length ? page.at(-1).id : null);
} });
assert.equal(calls.length, 7);
assert.equal(catalog.items.length, 500);
assert.equal(catalog.window.laboratoryCount, 1);
assert.equal(catalog.window.sourceLimitReached, false);
assert.equal(catalog.window.laboratoryLimitReached, false);
assert.equal(catalog.items.find((item) => item.source.id === "source-499").evidence[0].sessionId, "portable-after-legacy");
assert.deepEqual(catalog.unresolvedEvidence, []);
});
test("exactly one full page with no cursor is complete, not a guessed truncated window", async () => {
const catalog = await fetchObservatoryCatalog({ limit: 1, fetcher: async (url) =>
String(url).includes("scope=source")
? pageResponse([source("source", "2026-08-29T10:00:00Z")])
: pageResponse([evidence("result", "source", "2026-08-29T11:00:00Z")]),
});
assert.equal(catalog.window.sourceLimitReached, false);
assert.equal(catalog.window.laboratoryLimitReached, false);
});
test("paging rejects repeated records, cursor cycles, unsafe cursors and unversioned responses", async () => {
for (const failure of ["duplicate", "cycle", "unsafe", "unversioned", "too-large"]) {
let index = 0;
await assert.rejects(fetchObservatoryCatalog({ limit: 1, fetcher: async (url) => {
if (String(url).includes("scope=laboratory")) return pageResponse([]);
index += 1;
if (failure === "unversioned") return new Response(JSON.stringify({ items: [] }));
if (failure === "unsafe") return pageResponse([], "../../private");
const id = failure === "duplicate" ? "same" : `source-${index}`;
const rows = [source(id, "2026-08-29T10:00:00Z")];
if (failure === "too-large") rows.push(source("extra", "2026-08-29T10:00:00Z"));
return pageResponse(rows, failure === "cycle" ? "cycle" : id);
} }));
assert.ok(index <= 2);
}
});
test("the traversal cap is explicit and cannot confirm absence of a result", async () => {
let count = 0;
const catalog = await fetchObservatoryCatalog({ limit: 1, fetcher: async (url) => {
if (String(url).includes("scope=source")) return pageResponse([]);
count += 1;
return pageResponse([], `cursor-${count}`);
} });
assert.equal(count, 256);
assert.equal(catalog.window.laboratoryLimitReached, true);
assert.equal(observatoryCatalogConfirmsEvidenceDeletion(catalog, "missing"), false);
});
test("failure aborts the sibling traversal and an already aborted caller starts no requests", async () => {
const signals = [];
await assert.rejects(fetchObservatoryCatalog({ fetcher: async (url, init) => {
signals.push(init.signal);
if (String(url).includes("scope=source")) return new Response("unavailable", { status: 503 });
return pageResponse([]);
} }), /503/);
assert.ok(signals.every((signal) => signal.aborted));
const request = new AbortController();
request.abort();
await assert.rejects(fetchObservatoryCatalog({ signal: request.signal, fetcher: async () => {
assert.fail("aborted catalog cannot start a request");
} }), { name: "AbortError" });
});
test("Observatory applies an exact rename locally without mutating canonical identity", () => {
const catalog = buildObservatoryCatalog(
[source("source", "2026-08-29T10:00:00Z")],
[evidence("result", "source", "2026-08-29T11:00:00Z")],
);
const original = catalog.items[0].evidence[0];
const projected = applyObservatoryCatalogMutationOverlay(
catalog,
new Map([["result", {
kind: "rename",
displayName: "Операторское имя",
revision: 1,
}]]),
);
const renamed = findObservatoryEvidence(projected, "result");
assert.equal(renamed.label, "Операторское имя");
assert.equal(renamed.sessionId, original.sessionId);
assert.equal(renamed.lab, original.lab);
assert.equal(renamed.recordedRun, original.recordedRun);
assert.equal(projected.items[0].source, catalog.items[0].source);
assert.equal(projected.window.laboratoryCount, 1);
});
test("Observatory tombstone removes only its catalog evidence projection", () => {
const catalog = buildObservatoryCatalog(
[source("source", "2026-08-29T10:00:00Z")],
[
evidence("remove", "source", "2026-08-29T12:00:00Z"),
evidence("keep", "source", "2026-08-29T11:00:00Z"),
],
);
const projected = applyObservatoryCatalogMutationOverlay(
catalog,
new Map([["remove", { kind: "delete", revision: 1 }]]),
);
assert.equal(projected.items.length, 1);
assert.equal(projected.items[0].source, catalog.items[0].source);
assert.deepEqual(
projected.items[0].evidence.map(({ sessionId }) => sessionId),
["keep"],
);
assert.equal(projected.window.sourceCount, 1);
assert.equal(projected.window.laboratoryCount, 1);
});
test("Observatory confirms an ambiguous delete only from a complete LAB window", () => {
const bounded = buildObservatoryCatalog(
[source("source", "2026-08-29T10:00:00Z")],
[evidence("other", "source", "2026-08-29T11:00:00Z")],
1,
);
const complete = buildObservatoryCatalog(
[source("source", "2026-08-29T10:00:00Z")],
[],
);
assert.equal(observatoryCatalogConfirmsEvidenceDeletion(bounded, "result"), false);
assert.equal(observatoryCatalogConfirmsEvidenceDeletion(complete, "result"), true);
});
test("Observatory reconciliation cannot let a stale in-flight fetch undo a local mutation", () => {
const oldCatalog = buildObservatoryCatalog(
[source("source", "2026-08-29T10:00:00Z")],
[evidence("result", "source", "2026-08-29T11:00:00Z")],
);
const renameOverlay = new Map([["result", {
kind: "rename",
displayName: "Новое имя",
revision: 1,
}]]);
const staleRename = reconcileObservatoryCatalogMutationOverlay(
oldCatalog,
renameOverlay,
0,
);
assert.equal(findObservatoryEvidence(staleRename.catalog, "result").label, "Новое имя");
assert.equal(staleRename.overlay.has("result"), true);
const reconciledOldRename = reconcileObservatoryCatalogMutationOverlay(
oldCatalog,
renameOverlay,
1,
);
assert.equal(findObservatoryEvidence(reconciledOldRename.catalog, "result").label, "Новое имя");
assert.equal(reconciledOldRename.overlay.has("result"), true);
const renamedEvidence = evidence("result", "source", "2026-08-29T11:00:00Z");
renamedEvidence.label = "Новое имя";
const confirmedRename = reconcileObservatoryCatalogMutationOverlay(
buildObservatoryCatalog(
[source("source", "2026-08-29T10:00:00Z")],
[renamedEvidence],
),
staleRename.overlay,
1,
);
assert.equal(findObservatoryEvidence(confirmedRename.catalog, "result").label, "Новое имя");
assert.equal(confirmedRename.overlay.size, 0);
const deleteOverlay = new Map([["result", { kind: "delete", revision: 2 }]]);
const staleDelete = reconcileObservatoryCatalogMutationOverlay(
oldCatalog,
deleteOverlay,
1,
);
assert.equal(findObservatoryEvidence(staleDelete.catalog, "result"), null);
assert.equal(staleDelete.overlay.has("result"), true);
const reconciledOldDelete = reconcileObservatoryCatalogMutationOverlay(
oldCatalog,
deleteOverlay,
2,
);
assert.equal(findObservatoryEvidence(reconciledOldDelete.catalog, "result"), null);
assert.equal(reconciledOldDelete.overlay.has("result"), true);
const confirmedDelete = reconcileObservatoryCatalogMutationOverlay(
buildObservatoryCatalog(
[source("source", "2026-08-29T10:00:00Z")],
[],
),
staleDelete.overlay,
2,
);
assert.equal(findObservatoryEvidence(confirmedDelete.catalog, "result"), null);
assert.equal(confirmedDelete.overlay.size, 0);
});
@@ -0,0 +1,123 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let deleteObservatoryLabProjection;
let renameObservatoryLabProjection;
let ObservatoryCatalogMutationError;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
deleteObservatoryLabProjection,
renameObservatoryLabProjection,
ObservatoryCatalogMutationError,
} = await server.ssrLoadModule("/src/core/observatory/catalogMutations.ts"));
});
after(async () => {
await server?.close();
});
const sessionId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
const binding = {
kind: "canonical-recorded-rerun",
evidenceSessionId: sessionId,
sourceSessionId: "20260828T130511Z_viewer_live",
resultId: sessionId,
viewerProfile: "recorded-session",
timeline: "session_time",
activation: "explicit",
};
test("Observatory rename sends the closed projection-only contract", async () => {
const calls = [];
const result = await renameObservatoryLabProjection(binding, " Новый разбор ", {
fetcher: async (input, init) => {
calls.push({ input, init });
return Response.json({
schema_version: "missioncore.observatory-lab-projection/v1",
session_id: sessionId,
display_name: "Новый разбор",
});
},
});
assert.deepEqual(result, {
schemaVersion: "missioncore.observatory-lab-projection/v1",
sessionId,
displayName: "Новый разбор",
});
assert.equal(calls.length, 1);
assert.equal(calls[0].input, `/api/v1/observatory/lab-projections/${sessionId}`);
assert.equal(calls[0].init.method, "PATCH");
assert.equal(calls[0].init.headers.Accept, "application/json");
assert.equal(calls[0].init.headers["Content-Type"], "application/json");
assert.deepEqual(JSON.parse(calls[0].init.body), {
schema_version: "missioncore.observatory-lab-projection-rename/v1",
display_name: "Новый разбор",
});
});
test("Observatory delete accepts only an empty 204 response", async () => {
const calls = [];
await deleteObservatoryLabProjection(binding, {
fetcher: async (input, init) => {
calls.push({ input, init });
return new Response(null, { status: 204 });
},
});
assert.equal(calls.length, 1);
assert.equal(calls[0].input, `/api/v1/observatory/lab-projections/${sessionId}`);
assert.equal(calls[0].init.method, "DELETE");
await assert.rejects(
deleteObservatoryLabProjection(binding, {
fetcher: async () => new Response("unexpected", { status: 200 }),
}),
ObservatoryCatalogMutationError,
);
});
test("Observatory mutations fail closed before network access", async () => {
let calls = 0;
const fetcher = async () => {
calls += 1;
return new Response(null, { status: 204 });
};
await assert.rejects(
renameObservatoryLabProjection(binding, " ", { fetcher }),
/от 1 до 160/,
);
await assert.rejects(
deleteObservatoryLabProjection({ ...binding, resultId: "different" }, { fetcher }),
/не допущен/,
);
assert.equal(calls, 0);
});
test("Observatory mutations reject response drift and preserve API detail", async () => {
await assert.rejects(
renameObservatoryLabProjection(binding, "Разбор", {
fetcher: async () => Response.json({
schema_version: "missioncore.observatory-lab-projection/v1",
session_id: sessionId,
display_name: "Разбор",
unexpected: true,
}),
}),
/неизвестные поля/,
);
await assert.rejects(
deleteObservatoryLabProjection(binding, {
fetcher: async () => Response.json({ detail: "Проекция не принадлежит Обсерватории." }, { status: 409 }),
}),
(error) => error instanceof ObservatoryCatalogMutationError
&& error.status === 409
&& error.message === "Проекция не принадлежит Обсерватории.",
);
});
@@ -0,0 +1,96 @@
import assert from "node:assert/strict";
import React from "react";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let useObservatoryRecordedJobs;
let useObservatoryLaboratorySetups;
before(async () => {
server = await createServer({
appType: "custom", logLevel: "silent", server: { middlewareMode: true },
});
({ useObservatoryRecordedJobs } = await server.ssrLoadModule("/src/core/observatory/useObservatoryRecordedJobs.ts"));
({ useObservatoryLaboratorySetups } = await server.ssrLoadModule("/src/core/observatory/useObservatoryLaboratorySetups.ts"));
});
after(async () => { await server?.close(); });
// Deliberately inspect the render BEFORE effects/cleanup: stale state must already be hidden.
function renderBeforeEffects(hook, args, stateSlots) {
const internals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
const previous = internals.H;
let stateIndex = 0;
internals.H = {
useState: (initial) => [
stateIndex < stateSlots.length ? stateSlots[stateIndex++]
: typeof initial === "function" ? initial() : initial,
() => {},
],
useRef: (value) => ({ current: value }),
useMemo: (factory) => factory(),
useCallback: (fn) => fn,
useEffect: () => {},
};
try { return hook(...args); } finally { internals.H = previous; }
}
test("queue data and errors cannot survive a source/setup/version change for one render", () => {
const original = ["source-a", "profile-a", "a".repeat(64)];
const activeJob = { state: "running", publication: { state: "not-required" } };
const snapshot = {
selectionKey: JSON.stringify(original), jobs: [activeJob], state: "ready", error: "old error",
};
assert.equal(renderBeforeEffects(useObservatoryRecordedJobs, original, [snapshot]).activeJob, activeJob);
for (const next of [
["source-b", original[1], original[2]],
[original[0], "profile-b", original[2]],
[original[0], original[1], "b".repeat(64)],
["", "", ""],
]) {
const view = renderBeforeEffects(useObservatoryRecordedJobs, next, [snapshot]);
assert.deepEqual(view.jobs, []);
assert.equal(view.activeJob, null);
assert.equal(view.latestJob, null);
assert.equal(view.error, null);
assert.notEqual(view.state, "ready");
}
});
test("published, incompatible and previous-source profiles have no effective selection", () => {
const profile = {
setupId: "profile-a", origin: "portable-definition",
compatibility: { compatible: true },
runDefinition: { definitionSha256: "a".repeat(64) },
preflight: { outcome: "ready" },
};
function render(sourceId, setups) {
return renderBeforeEffects(useObservatoryLaboratorySetups, [sourceId], [
{ sourceSessionId: "source-a", setups }, "ready", null, "profile-a", 0, { kind: "idle" },
]);
}
assert.equal(render("source-a", [profile]).selectedSetupId, "profile-a");
for (const view of [
render("source-b", [profile]),
render("source-a", [{ ...profile, preflight: { outcome: "existing" } }]),
render("source-a", [{ ...profile, compatibility: { compatible: false } }]),
render("source-a", []),
]) {
assert.equal(view.selectedSetupId, "");
assert.equal(view.selectedSetup, null);
assert.deepEqual(view.selectableSetups, []);
}
});
test("historical failures are not presented as errors of a new operator action", () => {
const selection = ["source-a", "profile-a", "a".repeat(64)];
const snapshot = {
selectionKey: JSON.stringify(selection),
jobs: [{ jobId: "old-run", state: "failed", publication: { state: "not-required" } }],
state: "ready", error: null,
};
const view = renderBeforeEffects(useObservatoryRecordedJobs, selection, [snapshot]);
assert.equal(view.computationFailed, false);
assert.equal(view.error, null);
assert.equal(view.activeJob, null);
});
@@ -0,0 +1,591 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let fetchObservatoryLaboratorySetups;
let fetchObservatoryPortableLaboratorySetups;
let preflightObservatoryLaboratorySetup;
let selectableObservatorySetups;
let ObservatoryLaboratorySetupContractError;
const authority = {
commands_enabled: false,
actuation_allowed: false,
navigation_or_safety_accepted: false,
production_accepted: false,
};
function definition() {
return {
schema_version: "missioncore.observatory-run-definition/v1",
definition_id: "m49-tgs-full-shadow",
version: 1,
work_id: "m49-tgs-full-shadow",
source: {
session_id: "source-a",
label: "RAVNOVES00",
required_modalities: ["point-cloud", "trajectory", "video"],
},
configuration: [{ role: "primary-profile", sha256: "a".repeat(64) }],
authority,
definition_sha256: "b".repeat(64),
};
}
function setup() {
return {
setup_id: "m49-tgs-full-shadow-v1",
display_name: "M4.9T5",
description: "Сохранённый полный source-paced shadow.",
origin: "archived-definition",
source: {
session_id: "source-a",
label: "RAVNOVES00",
required_modalities: ["point-cloud", "trajectory", "video"],
},
run_definition: definition(),
compatibility: { compatible: true, reasons: [] },
executor: {
contour_id: "worker-006",
state: "not-installed",
reason_code: "laboratory-runner-adapter-not-installed",
reason: "Адаптер не установлен.",
},
preserved_results: [{
result_id: `m49-tgs-full-shadow-${"c".repeat(64)}`,
result_kind: "recorded-source-paced-tgs-shadow",
relation: "primary-visual",
access: "legacy-lab",
created_at_utc: "2026-08-26T20:27:19Z",
observatory_projection_available: false,
}],
preflight: {
outcome: "blocked",
action: "open-legacy",
reason: "Точный результат сохранён в legacy LAB.",
submission_allowed: false,
existing_result_ids: [],
},
authority,
};
}
function portableSetup() {
return {
setup_id: "lab-v1-eomt-ddrnet-portable-v1",
display_name: "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39",
description: "Проверка записанной K1-сессии моделями EoMT и DDRNet; только наблюдение.",
origin: "portable-definition",
source_requirements: {
plugin_id: "nodedc.device.xgrids-lixelkity-k1",
archive_id: "xgrids-k1.viewer-live.evidence",
required_modalities: ["point-cloud", "trajectory", "video"],
camera_source_id: "sensor.camera.right",
camera_semantic_channel_id: "camera.video.recorded",
recorded_media_type: 'video/mp4; codecs="avc1.641028"',
recorded_media_init_sha256: "1".repeat(64),
camera_width: 800,
camera_height: 600,
calibration_slot: "camera_1",
calibration_identity_sha256: "2".repeat(64),
exactly_one_media_epoch: true,
seekable: true,
},
run_definition: {
definition_id: "lab-v1-eomt-ddrnet-portable",
version: 1,
definition_sha256: "3".repeat(64),
result_schema: "missioncore.recorded-eomt-ddrnet-review/v2",
result_kind: "recorded-perception-qualification",
models: [
{
name: "EoMT Cityscapes Large 1024",
release_id: "eomt-cityscapes-large-1024-v1",
model_id: "tue-mps/cityscapes_semantic_eomt_large_1024",
architecture: "EomtForUniversalSegmentation",
},
{
name: "DDRNet-39",
release_id: "lab-v1-ddrnet-39-goose-fine-64-v1",
model_id: "goose-ddrnet-class-512",
architecture: "ddrnet_39",
},
],
},
source_compatibility: {
outcome: "pass",
compatible: true,
reason_code: "source-compatible",
reason: "Запись соответствует требованиям EoMT + DDRNet.",
},
executor: {
contour_id: "worker-006",
state: "not-installed",
ready: false,
reason_code: "eomt-executor-release-unsealed",
reason: "Immutable executor release не установлен.",
},
existing_results: [],
preflight: {
outcome: "blocked",
action: "blocked",
reason: "Вычислительный контур LAB V1 пока недоступен.",
submission_allowed: false,
existing_result_ids: [],
},
authority,
};
}
function portableM49Setup() {
const profile = portableSetup();
profile.setup_id = "m49-tgs-portable-v2";
profile.display_name = "M4.9T5 · TRAVEL TGS · CPU-only, без ML";
profile.description = "Динамический TGS-разбор записанной K1-сессии без ML; только наблюдение.";
profile.run_definition = {
definition_id: "m49-tgs-portable",
version: 2,
definition_sha256: "4".repeat(64),
result_schema: "missioncore.recorded-travel-tgs-review/v2",
result_kind: "recorded-source-paced-tgs-shadow",
models: [],
};
profile.source_compatibility.reason = "Запись соответствует требованиям TRAVEL TGS.";
profile.executor.reason_code = "m49-executor-release-unsealed";
profile.executor.reason = "Immutable executor release не установлен.";
profile.preflight.reason = "Переносимый вычислительный контур M4.9T5 пока недоступен.";
return profile;
}
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
fetchObservatoryLaboratorySetups,
fetchObservatoryPortableLaboratorySetups,
preflightObservatoryLaboratorySetup,
selectableObservatorySetups,
ObservatoryLaboratorySetupContractError,
} = await server.ssrLoadModule("/src/core/observatory/laboratorySetups.ts"));
});
after(async () => {
await server?.close();
});
test("Observatory setup catalog keeps definition identity separate from executor availability", async () => {
const calls = [];
const fetcher = async (input, init) => {
calls.push({ input: String(input), init });
return new Response(JSON.stringify({
schema_version: "missioncore.observatory-laboratory-setup-catalog/v1",
source_session_id: "source-a",
setups: [setup()],
authority,
}), { status: 200, headers: { "Content-Type": "application/json" } });
};
const catalog = await fetchObservatoryLaboratorySetups("source-a", { fetcher });
assert.equal(catalog.setups[0].runDefinition.definitionSha256, "b".repeat(64));
assert.equal(catalog.setups[0].executor.state, "not-installed");
assert.equal(catalog.setups[0].preflight.submissionAllowed, false);
assert.equal(catalog.setups[0].preservedResults[0].access, "legacy-lab");
assert.equal(
calls[0].input,
"/api/v1/observatory/laboratory-setups?source_session_id=source-a",
);
assert.equal(calls[0].init.method, "GET");
});
test("portable profiles report compatible source separately from unavailable Worker", async () => {
const calls = [];
const selected = (await fetchObservatoryPortableLaboratorySetups("source-a", {
fetcher: async (input, init) => {
calls.push({ input: String(input), init });
return new Response(JSON.stringify({
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
source_session_id: "source-a",
setups: [portableSetup(), portableM49Setup()],
authority,
}), { status: 200 });
},
})).setups[0];
assert.equal(selected.origin, "portable-definition");
assert.equal(selected.compatibility.compatible, true);
assert.equal(selected.executor.state, "not-installed");
assert.equal(selected.preflight.submissionAllowed, false);
assert.deepEqual(selected.runDefinition.models.map((model) => model.name), [
"EoMT Cityscapes Large 1024",
"DDRNet-39",
]);
const m49 = (await fetchObservatoryPortableLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
source_session_id: "source-a",
setups: [portableSetup(), portableM49Setup()],
authority,
}), { status: 200 }),
})).setups[1];
assert.equal(m49.displayName, "M4.9T5 · TRAVEL TGS · CPU-only, без ML");
assert.deepEqual(m49.runDefinition.models, []);
assert.equal(
calls[0].input,
"/api/v1/observatory/portable-laboratory-setups?source_session_id=source-a",
);
let preflightRequest;
const preflight = await preflightObservatoryLaboratorySetup("source-a", selected, {
fetcher: async (input, init) => {
preflightRequest = { input: String(input), init };
return new Response(JSON.stringify({
schema_version: "missioncore.observatory-run-preflight/v1",
source_session_id: "source-a",
setup_id: selected.setupId,
definition_sha256: "3".repeat(64),
check_sha256: null,
outcome: "blocked",
submission_allowed: false,
checks: [{
check_id: "executor",
outcome: "fail",
reason_code: "eomt-executor-release-unsealed",
message: "Immutable executor release не установлен.",
}],
existing_result_ids: [],
executor: portableSetup().executor,
authority,
}), { status: 200 });
},
});
assert.equal(preflightRequest.input, "/api/v1/observatory/run-preflights");
assert.equal(preflightRequest.init.method, "POST");
assert.equal(preflight.outcome, "blocked");
assert.equal(preflight.submissionAllowed, false);
assert.equal(preflight.checkSha256, null);
assert.equal(preflight.checks[0].outcome, "fail");
});
test("portable profile accepts a consistent ready projection", async () => {
const ready = portableSetup();
ready.executor = {
contour_id: "worker-006",
state: "ready",
ready: true,
reason_code: null,
reason: null,
};
ready.preflight = {
outcome: "ready",
action: "check",
reason: "Запись готова к постановке в очередь.",
submission_allowed: true,
existing_result_ids: [],
};
const catalog = await fetchObservatoryPortableLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
source_session_id: "source-a",
setups: [ready],
authority,
}), { status: 200 }),
});
assert.equal(catalog.setups[0].executor.state, "ready");
assert.equal(catalog.setups[0].preflight.outcome, "ready");
assert.equal(catalog.setups[0].preflight.submissionAllowed, true);
});
test("portable LAB V1 rejects an unbound existing result projection", async () => {
const spoofed = portableSetup();
spoofed.existing_results = [{
result_id: `legacy-vegetation-${"4".repeat(64)}`,
result_kind: "recorded-perception-qualification",
created_at_utc: "2026-08-30T18:16:00Z",
}];
spoofed.preflight = {
outcome: "existing",
action: "open-existing",
reason: "Результат якобы готов.",
submission_allowed: false,
existing_result_ids: [spoofed.existing_results[0].result_id],
};
await assert.rejects(
fetchObservatoryPortableLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
source_session_id: "source-a",
setups: [spoofed],
authority,
}), { status: 200 }),
}),
/значение изменилось|значение не поддерживается|обнаружены неизвестные поля/,
);
});
test("portable LAB V1 rejects heavyweight compatibility evidence", async () => {
const heavyweight = portableSetup();
heavyweight.source_compatibility.evidence = {
frame_count: 6830,
timeline_start_seconds: 0,
timeline_end_seconds: 819,
};
await assert.rejects(
fetchObservatoryPortableLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
source_session_id: "source-a",
setups: [heavyweight],
authority,
}), { status: 200 }),
}),
/неизвестные поля/,
);
});
function cachedPortableSetup() {
const setup = portableSetup();
setup.existing_results = [{
result_id: "portable-result-001",
result_kind: setup.run_definition.result_kind,
relation: "exact-recorded-computation",
access: "observatory",
created_at_utc: "2026-09-03T00:00:00Z",
observatory_projection_available: true,
identity: {
job_id: "observatory-run-001",
source_session_id: "source-a",
source_catalog_sha256: "a".repeat(64),
source_bundle_sha256: "b".repeat(64),
source_capability_manifest_sha256: "c".repeat(64),
setup_id: setup.setup_id,
definition_sha256: setup.run_definition.definition_sha256,
package_sha256: "d".repeat(64),
artifact_manifest_id: "e".repeat(64),
},
}];
setup.preflight = {
outcome: "existing", action: "open-existing",
reason: "Точный результат проверен.", submission_allowed: false,
existing_result_ids: [setup.existing_results[0].result_id],
};
return setup;
}
function fetchCachedPortable(setup) {
return fetchObservatoryPortableLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
source_session_id: "source-a", setups: [setup], authority,
}), { status: 200 }),
});
}
test("portable exact cached result is readable without an installed executor", async () => {
const catalog = await fetchCachedPortable(cachedPortableSetup());
const setup = catalog.setups[0];
assert.equal(setup.executor.state, "not-installed");
assert.equal(setup.preflight.outcome, "existing");
assert.equal(setup.preflight.submissionAllowed, false);
assert.deepEqual(setup.preflight.existingResultIds, ["portable-result-001"]);
assert.equal(setup.preservedResults[0].access, "observatory");
});
test("selector contains only compatible uncalculated portable definitions", async () => {
const uncached = (await fetchCachedPortable(portableSetup())).setups[0];
const cached = (await fetchCachedPortable(cachedPortableSetup())).setups[0];
const currentVersion = {
...uncached,
runDefinition: { ...uncached.runDefinition, version: 2, definitionSha256: "f".repeat(64) },
};
const catalog = (setups) => ({ sourceSessionId: "source-a", setups });
assert.deepEqual(selectableObservatorySetups(null), []);
assert.deepEqual(selectableObservatorySetups(catalog([])), []);
assert.deepEqual(selectableObservatorySetups(catalog([cached])), []);
// Same display label and setup ID, but no verified cache hit for the new definition.
assert.deepEqual(selectableObservatorySetups(catalog([currentVersion])), [currentVersion]);
assert.deepEqual(selectableObservatorySetups(catalog([
cached,
{ ...uncached, setupId: "remaining-profile" },
])).map((s) => s.setupId), ["remaining-profile"]);
assert.deepEqual(selectableObservatorySetups(catalog([
{ ...uncached, compatibility: { compatible: false, reasons: [] } },
{ ...uncached, origin: "archived-definition" },
{ ...uncached, origin: "existing-result" },
{ ...uncached, runDefinition: null },
])), []);
// Worker availability does not masquerade as completed computation.
assert.equal(uncached.executor.state, "not-installed");
assert.deepEqual(selectableObservatorySetups(catalog([uncached])), [uncached]);
// A missing/deleted/unpublished projection is not a completed result.
assert.deepEqual(selectableObservatorySetups(catalog([{
...uncached, preservedResults: cached.preservedResults,
}])).map((s) => s.setupId), [uncached.setupId]);
});
for (const [label, change] of [
["another source", (s) => { s.existing_results[0].identity.source_session_id = "source-b"; }],
["another setup", (s) => { s.existing_results[0].identity.setup_id = "another-profile"; }],
["another version", (s) => { s.existing_results[0].identity.definition_sha256 = "f".repeat(64); }],
["bad package digest", (s) => { s.existing_results[0].identity.package_sha256 = "not-a-digest"; }],
["unavailable artifact", (s) => { s.existing_results[0].observatory_projection_available = false; }],
["missing binding", (s) => { delete s.existing_results[0].identity; }],
["unrelated result ID", (s) => { s.preflight.existing_result_ids = ["different-result"]; }],
["duplicate result", (s) => {
s.existing_results.push(s.existing_results[0]);
s.preflight.existing_result_ids.push(s.preflight.existing_result_ids[0]);
}],
["contradictory action", (s) => { s.preflight.action = "check"; }],
["cached but queueable", (s) => { s.preflight.submission_allowed = true; }],
]) {
test(`portable cached result rejects ${label}`, async () => {
const setup = cachedPortableSetup();
change(setup);
await assert.rejects(fetchCachedPortable(setup));
});
}
test("Observatory preflight sends the exact selected definition and never submits a run", async () => {
const selected = (await fetchObservatoryLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-laboratory-setup-catalog/v1",
source_session_id: "source-a",
setups: [setup()],
authority,
}), { status: 200 }),
})).setups[0];
let request;
const preflight = await preflightObservatoryLaboratorySetup("source-a", selected, {
fetcher: async (input, init) => {
request = { input: String(input), init };
return new Response(JSON.stringify({
schema_version: "missioncore.observatory-run-preflight/v1",
source_session_id: "source-a",
setup_id: selected.setupId,
definition_sha256: "b".repeat(64),
check_sha256: null,
outcome: "blocked",
submission_allowed: false,
checks: [{
check_id: "executor",
outcome: "fail",
reason_code: "laboratory-runner-adapter-not-installed",
message: "Адаптер не установлен.",
}],
existing_result_ids: [],
executor: setup().executor,
authority,
}), { status: 200 });
},
});
assert.equal(request.input, "/api/v1/observatory/run-preflights");
assert.equal(request.init.method, "POST");
assert.deepEqual(JSON.parse(request.init.body), {
schema_version: "missioncore.observatory-run-preflight-request/v1",
source_session_id: "source-a",
setup_id: "m49-tgs-full-shadow-v1",
definition_sha256: "b".repeat(64),
});
assert.equal(preflight.outcome, "blocked");
assert.equal(preflight.submissionAllowed, false);
assert.equal(preflight.checkSha256, null);
});
test("Observatory dynamic preflight admits only an explicit queueable response", async () => {
const selected = (await fetchObservatoryLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-laboratory-setup-catalog/v1",
source_session_id: "source-a",
setups: [setup()],
authority,
}), { status: 200 }),
})).setups[0];
const preflight = await preflightObservatoryLaboratorySetup("source-a", selected, {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-run-preflight/v1",
source_session_id: "source-a",
setup_id: selected.setupId,
definition_sha256: "b".repeat(64),
outcome: "queueable",
submission_allowed: true,
checks: [{
check_id: "durable-queue",
outcome: "pass",
reason_code: "exact-binding-ready",
message: "Точный источник и очередь готовы.",
}],
existing_result_ids: [],
executor: setup().executor,
authority,
}), { status: 200 }),
});
assert.equal(preflight.outcome, "queueable");
assert.equal(preflight.submissionAllowed, true);
assert.equal(preflight.checkSha256, null);
});
test("Observatory setup contract rejects authority escalation and response drift", async () => {
const escalated = setup();
escalated.authority = { ...authority, commands_enabled: true };
await assert.rejects(
fetchObservatoryLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-laboratory-setup-catalog/v1",
source_session_id: "source-a",
setups: [escalated],
authority,
}), { status: 200 }),
}),
ObservatoryLaboratorySetupContractError,
);
const drifted = setup();
drifted.unexpected = true;
await assert.rejects(
fetchObservatoryLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-laboratory-setup-catalog/v1",
source_session_id: "source-a",
setups: [drifted],
authority,
}), { status: 200 }),
}),
ObservatoryLaboratorySetupContractError,
);
const selected = (await fetchObservatoryLaboratorySetups("source-a", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-laboratory-setup-catalog/v1",
source_session_id: "source-a",
setups: [setup()],
authority,
}), { status: 200 }),
})).setups[0];
await assert.rejects(
preflightObservatoryLaboratorySetup("source-a", selected, {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-run-preflight/v1",
source_session_id: "source-a",
setup_id: selected.setupId,
definition_sha256: "c".repeat(64),
outcome: "blocked",
submission_allowed: false,
checks: [],
existing_result_ids: [],
executor: setup().executor,
authority,
}), { status: 200 }),
}),
ObservatoryLaboratorySetupContractError,
);
});
@@ -0,0 +1,277 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let fetchObservatoryRecordedJobs;
let retryObservatoryRecordedJobPublication;
let submitObservatoryRecordedJob;
let ObservatoryRecordedJobContractError;
const authority = {
commands_enabled: false,
actuation_allowed: false,
navigation_or_safety_accepted: false,
production_accepted: false,
};
function job(state = "queued") {
return {
schema_version: "missioncore.observatory-recorded-job/v1",
job_id: "observatory-run-0123456789abcdef0123456789abcdef",
idempotency_key: "observatory-ui:source-a:m49-tgs:request-a",
request_sha256: "1".repeat(64),
identity_sha256: "2".repeat(64),
submission_receipt_sha256: "3".repeat(64),
source: {
session_id: "source-a",
catalog_sha256: "4".repeat(64),
bundle_sha256: "5".repeat(64),
capability_manifest_sha256: "6".repeat(64),
adapter: {
adapter_id: "ravnoves00-m49-source-pack",
version: 1,
adapter_sha256: "7".repeat(64),
},
},
setup: {
setup_id: "m49-tgs",
definition_id: "m49-tgs",
definition_version: 1,
definition_sha256: "8".repeat(64),
},
executor: {
release_id: "m49-tgs-release",
release_sha256: "9".repeat(64),
image_sha256: "a".repeat(64),
model_release_ids: [],
learned_models: [],
model_manifest_sha256: "b".repeat(64),
resource_profile_id: "m49-tgs-cpu",
resource_profile_sha256: "c".repeat(64),
},
checkpoint_policy: {
mode: "non-checkpointable",
allowed_checkpoints: [],
last_checkpoint_id: null,
},
priority: { class: "recorded", rank: 100, server_owned: true },
state,
preemption_requested: state === "preemption-pending",
restart_from_zero: state === "paused",
preemption_receipt_sha256: null,
claim_generation: 0,
claim_lease: null,
result: state === "succeeded"
? { result_id: "m49-result", sha256: "d".repeat(64) }
: null,
publication: {
state: "not-required",
attempts: 0,
error: null,
published_at_utc: null,
},
terminal: state === "failed"
? { code: "worker-failed", message: "Worker завершил расчёт с ошибкой." }
: null,
created_at_utc: "2026-08-31T10:00:00Z",
updated_at_utc: "2026-08-31T10:00:01Z",
authority,
};
}
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
fetchObservatoryRecordedJobs,
retryObservatoryRecordedJobPublication,
submitObservatoryRecordedJob,
ObservatoryRecordedJobContractError,
} = await server.ssrLoadModule("/src/core/observatory/recordedJobs.ts"));
});
after(async () => {
await server?.close();
});
test("Observatory reads the exact session/setup queue without polling", async () => {
const calls = [];
const jobs = await fetchObservatoryRecordedJobs("source-a", "m49-tgs", {
fetcher: async (input, init) => {
calls.push({ input: String(input), init });
return new Response(JSON.stringify({
schema_version: "missioncore.observatory-recorded-job-list/v1",
items: [job("running")],
authority,
}), { status: 200 });
},
});
assert.equal(calls.length, 1);
assert.equal(
calls[0].input,
"/api/v1/observatory/runs?source_session_id=source-a&setup_id=m49-tgs&limit=20",
);
assert.equal(calls[0].init.method, "GET");
assert.equal(jobs[0].state, "running");
assert.equal(jobs[0].definitionSha256, "8".repeat(64));
});
test("Observatory submits only public identities and accepts every durable state", async () => {
const states = [
"accepted",
"queued",
"claimed",
"running",
"paused",
"preemption-pending",
"succeeded",
"failed",
"reconciliation-required",
];
for (const state of states) {
let request;
const result = await submitObservatoryRecordedJob(
"source-a",
"m49-tgs",
"observatory-ui:source-a:m49-tgs:request-a",
null,
{
fetcher: async (input, init) => {
request = { input: String(input), init };
return new Response(JSON.stringify(job(state)), { status: 200 });
},
},
);
assert.equal(result.state, state);
assert.equal(request.input, "/api/v1/observatory/runs");
assert.equal(request.init.method, "POST");
assert.deepEqual(JSON.parse(request.init.body), {
schema_version: "missioncore.observatory-recorded-run-submit/v1",
idempotency_key: "observatory-ui:source-a:m49-tgs:request-a",
source_session_id: "source-a",
setup_id: "m49-tgs",
});
}
});
test("Observatory queue contract rejects authority escalation and response drift", async () => {
const escalated = job();
escalated.authority = { ...authority, commands_enabled: true };
await assert.rejects(
fetchObservatoryRecordedJobs("source-a", "m49-tgs", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.observatory-recorded-job-list/v1",
items: [escalated],
authority,
}), { status: 200 }),
}),
ObservatoryRecordedJobContractError,
);
const drifted = job();
drifted.worker_command = "forbidden";
await assert.rejects(
submitObservatoryRecordedJob(
"source-a",
"m49-tgs",
"observatory-ui:source-a:m49-tgs:request-a",
null,
{ fetcher: async () => new Response(JSON.stringify(drifted), { status: 200 }) },
),
ObservatoryRecordedJobContractError,
);
});
test("portable submission carries the exact definition/check fence", async () => {
let request;
await submitObservatoryRecordedJob(
"source-a",
"lab-v1-eomt-ddrnet-portable-v1",
"observatory-ui:source-a:lab-v1:request-a",
{
definitionSha256: "e".repeat(64),
checkSha256: "f".repeat(64),
},
{
fetcher: async (input, init) => {
request = { input: String(input), init };
const response = job("queued");
response.setup.setup_id = "lab-v1-eomt-ddrnet-portable-v1";
response.setup.definition_sha256 = "e".repeat(64);
response.source.session_id = "source-a";
return new Response(JSON.stringify(response), { status: 202 });
},
},
);
assert.deepEqual(JSON.parse(request.init.body), {
schema_version: "missioncore.observatory-recorded-run-submit/v1",
idempotency_key: "observatory-ui:source-a:lab-v1:request-a",
source_session_id: "source-a",
setup_id: "lab-v1-eomt-ddrnet-portable-v1",
definition_sha256: "e".repeat(64),
check_sha256: "f".repeat(64),
});
});
test("publication retry never submits a second compute request", async () => {
let request;
const response = job("succeeded");
response.publication = {
state: "published",
attempts: 2,
error: null,
published_at_utc: "2026-09-01T12:00:00Z",
};
const retried = await retryObservatoryRecordedJobPublication(response.job_id, {
fetcher: async (input, init) => {
request = { input: String(input), init };
return new Response(JSON.stringify(response), { status: 200 });
},
});
assert.equal(
request.input,
`/api/v1/observatory/runs/${response.job_id}/publication/retry`,
);
assert.equal(request.init.method, "POST");
assert.equal(request.init.body, undefined);
assert.equal(retried.publication.state, "published");
});
test("queue query and response are fenced to the selected definition", async () => {
const oldVersion = job("succeeded");
oldVersion.setup.definition_sha256 = "f".repeat(64);
const otherSource = job("running");
otherSource.source.session_id = "source-b";
const otherSetup = job("running");
otherSetup.setup.setup_id = "other-setup";
let url;
const jobs = await fetchObservatoryRecordedJobs("source-a", "m49-tgs", {
definitionSha256: "8".repeat(64),
fetcher: async (input) => {
url = new URL(String(input), "http://localhost");
return new Response(JSON.stringify({
schema_version: "missioncore.observatory-recorded-job-list/v1",
items: [oldVersion, otherSource, otherSetup, job("queued")], authority,
}));
},
});
assert.equal(url.searchParams.get("definition_sha256"), "8".repeat(64));
assert.deepEqual(jobs.map((j) => j.state), ["queued"]);
});
test("portable submit rejects a successful response for an old definition", async () => {
await assert.rejects(submitObservatoryRecordedJob(
"source-a", "m49-tgs", "test-request",
{ definitionSha256: "f".repeat(64), checkSha256: "e".repeat(64) },
{ fetcher: async () => new Response(JSON.stringify(job("succeeded"))) },
), ObservatoryRecordedJobContractError);
});
@@ -0,0 +1,83 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let decode;
let label;
let fetchProgress;
const job = {
jobId: "observatory-run-" + "1".repeat(32),
sourceSessionId: "source-a", setupId: "m49-tgs",
definitionSha256: "a".repeat(64), claimGeneration: 2, state: "running",
publication: { state: "not-required" },
};
function view() {
return {
schema_version: "missioncore.observatory-recorded-progress-view/v1",
job_id: job.jobId, source_session_id: job.sourceSessionId, setup_id: job.setupId,
definition_sha256: job.definitionSha256, claim_generation: 2, state: "running",
received_at_utc: "2026-09-03T12:00:00Z", age_seconds: 0.5,
progress: {
schema_version: "missioncore.observatory-recorded-progress/v1",
claim_generation: 2, sequence: 4, phase_index: 2,
phase: "computing", unit: "frames", completed: 3, total: 10,
elapsed_seconds: 8, phase_elapsed_seconds: 5,
},
};
}
before(async () => {
server = await createServer({ configFile: false, logLevel: "silent", server: { middlewareMode: true } });
const module = await server.ssrLoadModule("/src/core/observatory/recordedProgress.ts");
decode = module.decodeRecordedProgress;
label = module.recordedProgressLabel;
fetchProgress = module.fetchRecordedProgress;
});
after(async () => { await server?.close(); });
test("actual frame count is separate from completion and publication", () => {
const progress = decode(view(), job);
assert.equal(label(job, progress), "Обрабатываем запись · 3 / 10 кадров");
assert.equal(label({ ...job, publication: { state: "pending" } }, progress), "Сохраняем результат");
assert.doesNotMatch(label(job, progress), /%|завершён|FPS|ETA/);
});
test("unknown total never becomes a synthetic percentage", () => {
const value = view();
value.progress.total = null;
assert.equal(label(job, decode(value, job)), "Обрабатываем запись · 3 кадров");
value.progress.completed = 0;
assert.equal(label(job, decode(value, job)), "Обрабатываем запись");
});
test("old source definition or attempt cannot flash as current progress", () => {
for (const delta of [
{ job_id: "other" }, { source_session_id: "other" }, { setup_id: "other" },
{ definition_sha256: "b".repeat(64) }, { claim_generation: 1 },
]) assert.throws(() => decode({ ...view(), ...delta }, job));
assert.equal(decode({ ...view(), state: "succeeded" }, job), null);
assert.equal(label({ ...job, claimGeneration: 3 }, decode(view(), job)), "Ожидаем данные расчёта");
});
test("invalid counters, invented phase and extra fields fail closed", () => {
for (const delta of [
{ completed: true }, { completed: 11 }, { completed: -1 }, { total: 0 },
{ completed: Number.MAX_SAFE_INTEGER + 1 }, { phase: "done" }, { path: "/private" },
{ phase_elapsed_seconds: 9 }, { claim_generation: 3 },
]) assert.throws(() => decode({ ...view(), progress: { ...view().progress, ...delta } }, job));
});
test("missing and stale telemetry show no fabricated advancement", () => {
assert.equal(decode({ ...view(), progress: null, age_seconds: null, received_at_utc: null }, job), null);
assert.equal(label(job, null), "Ожидаем данные расчёта");
assert.equal(label(job, decode({ ...view(), age_seconds: 30 }, job)), "Ожидаем обновление прогресса");
});
test("terminal jobs keep their real state when progress telemetry is absent", () => {
assert.equal(label({ ...job, state: "failed" }, null), "Ошибка расчёта");
assert.equal(label({ ...job, state: "succeeded" }, null), "Расчёт завершён");
});
test("progress is a read-only request bound to the active job", async () => {
const calls = [];
const progress = await fetchProgress(job, { fetcher: async (path, init) => {
calls.push([path, init.method ?? "GET"]);
return new Response(JSON.stringify(view()));
} });
assert.deepEqual(calls, [[`/api/v1/observatory/runs/${job.jobId}/progress`, "GET"]]);
assert.equal(progress.completed, 3);
});
@@ -0,0 +1,292 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let admitObservatoryRecordedRunReview;
let fetchObservatoryRecordedRunReview;
let observatoryRecordedRunBinding;
let ObservatoryRecordedRunContractError;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
admitObservatoryRecordedRunReview,
fetchObservatoryRecordedRunReview,
observatoryRecordedRunBinding,
ObservatoryRecordedRunContractError,
} = await server.ssrLoadModule("/src/core/observatory/recordedRun.ts"));
});
after(async () => {
await server?.close();
});
const sourceSessionId = "20260828T130511Z_viewer_live";
const resultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
function canonicalProvenance() {
const identitySha256 = resultId.slice("lab-v1-vegetation-shadow-".length);
return {
schema_version: "missioncore.canonical-recorded-lab-projection/v1",
evidence_identity_sha256: identitySha256,
result_document_sha256: "a".repeat(64),
replay_capability: {
schema_version: "missioncore.observation-lab-replay-capability/v1",
kind: "canonical-recorded-rerun",
viewer_profile: "recorded-session",
timeline: "session_time",
activation: "explicit",
commands_enabled: false,
},
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
actuation_accepted: false,
},
method: {
schema_version: "missioncore.laboratory-method/v1",
completeness: "legacy-partial",
execution_class: "ai-inference",
pipeline_id: "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1",
components: [{
kind: "source",
name: "sealed full-route LAB result",
version: "missioncore.lab-v1-vegetation-shadow/v1",
role: "immutable Session catalog projection",
identity_sha256: identitySha256,
}],
},
};
}
function lab(overrides = {}) {
return {
labId: "LAB V1",
sourceSessionId,
resultKind: "recorded-perception-qualification",
resultId,
sourceResultId: `lab-v1-vegetation-shadow-${"9".repeat(64)}`,
configSha256: null,
runCreatedAtUtc: "2026-08-29T18:05:11.329061+00:00",
publishedAtUtc: "2026-08-30T12:00:00.000Z",
replayCapability: {
schemaVersion: "missioncore.observation-lab-replay-capability/v1",
kind: "canonical-recorded-rerun",
viewerProfile: "recorded-session",
timeline: "session_time",
activation: "explicit",
commandsEnabled: false,
},
provenance: canonicalProvenance(),
...overrides,
};
}
function portableCapability() {
return {
schemaVersion: "missioncore.observation-lab-replay-capability/v2",
kind: "portable-result-review",
viewerProfile: "portable-result",
timeline: "result-defined",
activation: "explicit",
commandsEnabled: false,
};
}
function portableCapabilityDocument() {
return {
schema_version: "missioncore.observation-lab-replay-capability/v2",
kind: "portable-result-review",
viewer_profile: "portable-result",
timeline: "result-defined",
activation: "explicit",
commands_enabled: false,
};
}
function portableLab(portableResultId, definitionSha256) {
return lab({
labId: "M4.9",
resultId: portableResultId,
resultKind: "recorded-route-analysis",
sourceResultId: null,
configSha256: definitionSha256,
replayCapability: portableCapability(),
provenance: {
schema_version: "missioncore.observatory-portable-result-publication/v1",
authority: {},
calculation_profile: {},
calculation_profile_sha256: "b".repeat(64),
job: {},
source: { session_id: sourceSessionId },
run_definition: { definition_sha256: definitionSha256 },
result_package: {
manifest_sha256: "c".repeat(64),
artifact_manifest_id: "d".repeat(64),
},
replay_capability: portableCapabilityDocument(),
storage: {},
method: {},
},
});
}
test("Observatory admits only the exact typed canonical recorded run", () => {
const binding = observatoryRecordedRunBinding(resultId, lab());
assert.deepEqual(binding, {
kind: "canonical-recorded-rerun",
evidenceSessionId: resultId,
sourceSessionId,
resultId,
viewerProfile: "recorded-session",
timeline: "session_time",
activation: "explicit",
});
assert.equal(
observatoryRecordedRunBinding(resultId, lab({ replayCapability: null })),
null,
);
assert.throws(
() => observatoryRecordedRunBinding("different-session", lab()),
ObservatoryRecordedRunContractError,
);
assert.throws(
() => observatoryRecordedRunBinding(
resultId,
lab({ sourceSessionId: "RAVNOVES004TREE" }),
),
ObservatoryRecordedRunContractError,
);
assert.throws(
() => observatoryRecordedRunBinding(
resultId,
lab({ configSha256: "a".repeat(64) }),
),
ObservatoryRecordedRunContractError,
);
assert.throws(
() => observatoryRecordedRunBinding(
resultId,
lab({ sourceResultId: "legacy-result" }),
),
ObservatoryRecordedRunContractError,
);
const invalidProvenance = canonicalProvenance();
invalidProvenance.method.components[0].identity_sha256 = "0".repeat(64);
assert.throws(
() => observatoryRecordedRunBinding(
resultId,
lab({ provenance: invalidProvenance }),
),
ObservatoryRecordedRunContractError,
);
});
test("Observatory rechecks the selected source and sealed review before mounting replay", () => {
const binding = observatoryRecordedRunBinding(resultId, lab());
const review = { sessionId: sourceSessionId, frameCount: 6830 };
assert.equal(
admitObservatoryRecordedRunReview(
binding,
sourceSessionId,
{ resultId, routeFullReview: review },
),
review,
);
assert.throws(
() => admitObservatoryRecordedRunReview(
binding,
"another-session",
{ resultId, routeFullReview: review },
),
ObservatoryRecordedRunContractError,
);
assert.throws(
() => admitObservatoryRecordedRunReview(
binding,
sourceSessionId,
{ resultId, routeFullReview: { ...review, sessionId: "another-session" } },
),
ObservatoryRecordedRunContractError,
);
});
test("Observatory selects and strictly decodes a portable result viewer by capability", async () => {
const portableResultId = "portable-result-a";
const definitionSha256 = "e".repeat(64);
const binding = observatoryRecordedRunBinding(
portableResultId,
portableLab(portableResultId, definitionSha256),
);
assert.deepEqual(binding, {
kind: "portable-result-review",
evidenceSessionId: portableResultId,
sourceSessionId,
resultId: portableResultId,
viewerProfile: "portable-result",
timeline: "result-defined",
activation: "explicit",
definitionSha256,
});
let requestedUrl = null;
const review = await fetchObservatoryRecordedRunReview(binding, {
selectedSourceSessionId: sourceSessionId,
fetcher: async (url) => {
requestedUrl = url;
return {
ok: true,
status: 200,
json: async () => ({
schema_version: "missioncore.observatory-portable-result-view/v1",
result_id: portableResultId,
source_session_id: sourceSessionId,
result_kind: "recorded-route-analysis",
definition_sha256: definitionSha256,
viewer_capability: portableCapabilityDocument(),
calculation_profile: { name: "M4.9" },
artifact_manifest_id: "d".repeat(64),
artifacts: [{
role: "result-document",
media_type: "application/json",
sha256: "f".repeat(64),
byte_length: 42,
}],
result_document: { verdict: "reviewed" },
}),
};
},
});
assert.equal(
requestedUrl,
`/api/v1/observatory/portable-results/${portableResultId}`,
);
assert.equal(review.kind, "portable-result");
assert.deepEqual(review.resultDocument, { verdict: "reviewed" });
assert.deepEqual(review.artifacts, [{
role: "result-document",
mediaType: "application/json",
sha256: "f".repeat(64),
byteLength: 42,
}]);
});
test("Observatory run admission remains metadata-only until explicit UI activation", async () => {
const source = await readFile(
new URL("../src/core/observatory/recordedRun.ts", import.meta.url),
"utf8",
);
assert.match(source, /fetchVegetationShadowResultMetadata/);
assert.doesNotMatch(
source,
/fetchVegetationShadowResult\(|advanced-index|resolveObservationSessionReplay|resolveCanonicalLabReplay|RerunViewport|recording\.rrd/,
);
});
@@ -0,0 +1,369 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let productModel;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
productModel = await server.ssrLoadModule("/src/productModel.ts");
});
after(async () => {
await server?.close();
});
async function read(relativePath) {
return readFile(new URL(`../src/${relativePath}`, import.meta.url), "utf8");
}
test("Observatory is the third independent Polygon workspace", () => {
assert.deepEqual(
productModel.workspacesForRoot("polygon").map(({ id }) => id),
["lab-archive", "simulations", "observatory"],
);
assert.deepEqual(
productModel.workspaceById("observatory"),
{
id: "observatory",
root: "polygon",
label: "Обсерватория",
title: "AI inference",
eyebrow: "ТЕСТОВЫЙ КОНТУР / ОБСЕРВАТОРИЯ",
description: "Сессии и квалификация без доступа к управлению.",
icon: "eye",
kind: "observatory",
groups: [],
},
);
assert.equal(productModel.workspaceById("lab-archive").kind, "lab-archive");
});
test("Observatory mounts the one shared canonical replay only after explicit admission", async () => {
const [
app,
workspaceHub,
workspace,
hook,
recordedRun,
sharedReplay,
legacyReplayWrapper,
viewerProfiles,
styles,
] = await Promise.all([
read("App.tsx"),
read("workspaces/Workspaces.tsx"),
read("workspaces/observatory/ObservatoryWorkspace.tsx"),
read("core/observatory/useObservatoryCatalog.ts"),
read("core/observatory/recordedRun.ts"),
read("components/laboratory/CanonicalResultRerunReplay.tsx"),
read("workspaces/laboratory/CanonicalVegetationRerunReplay.tsx"),
read("core/observation/viewerProfile.ts"),
read("styles/observatory.css"),
]);
assert.match(app, /activeDefinition\.kind === "observatory"[\s\S]*Только наблюдение/);
assert.doesNotMatch(app, /\["recordings", "lab-archive", "observatory"\]/);
assert.match(workspaceHub, /case "observatory":[\s\S]*<ObservatoryWorkspace/);
assert.match(workspace, /useObservatoryCatalog/);
assert.doesNotMatch(workspace, /observatory-lead/);
assert.doesNotMatch(workspace, /Расчёт записанных маршрутов выбранными профилями/);
assert.match(workspace, /Связанных результатов нет/);
assert.match(workspace, /не является выводом о качестве/);
assert.match(workspace, /function compositionProducedEvidence\(/);
assert.match(workspace, /publishedAt >= compositionCreatedAt/);
assert.match(workspace, /const presentedCompositionRuns = aiJobsController\.runs/);
assert.match(workspace, /presentedEvidence = \(selectedSession\?\.evidence \?\? \[\]\)\.filter/);
assert.match(workspace, /!presentedCompositionRuns\.some\([\s\S]*compositionProducedEvidence\(run, evidence\)/);
assert.doesNotMatch(workspace, /MAX_PRESENTED_EVIDENCE|\.evidence\.slice\(/);
assert.match(workspace, /Полнота исторических/);
assert.match(workspace, /вне текущего загруженного среза/);
assert.match(workspace, /observatory-notice__copy/);
assert.match(workspace, /observatory-evidence-card/);
assert.match(workspace, /observatory-session-stack/);
assert.match(workspace, /observatory-session-summary__facts/);
assert.match(workspace, /observatory-evidence-card__copy/);
assert.match(
workspace,
/function evidenceConfigurationLabel[\s\S]*calculationProfile\?\.displayName[\s\S]*return profileName \?\? evidence\.label/,
);
assert.match(
workspace,
/<strong>\{evidenceConfigurationLabel\([\s\S]*evidence,[\s\S]*aiJobsController\.moduleLabelsBySetup,[\s\S]*aiJobsController\.jobs,[\s\S]*\)\}<\/strong>/,
);
assert.doesNotMatch(workspace, /evidenceConfigurationLabel\([^)]*,\s*selectedSession\.source\.label/);
assert.match(
workspace,
/evidence\.recordedRun \? \([\s\S]*name="trash"[\s\S]*name="edit"[\s\S]*Открыть визуальный разбор:[\s\S]*name="eye"/,
);
assert.doesNotMatch(workspace, /observatory-evidence-card[^>]*tone="soft"/);
assert.match(workspace, /Открыть визуальный разбор/);
assert.match(workspace, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/);
assert.match(workspace, /<PortableResultReplay review=\{replay\.review\}/);
const portable = await read("components/laboratory/PortableResultReplay.tsx");
assert.doesNotMatch(portable, /Документ результата|JSON.stringify|StatusBadge/);
assert.match(portable, /missioncore.recorded-eomt-ddrnet-review\/v2/);
assert.match(portable, /sourceKind: "portable-semantic"/);
assert.match(portable, /sourceKind: "portable-objects"/);
assert.match(portable, /costmap=\{tgs\} semantics=\{semantics\}/);
assert.match(portable, /detections=\{objects\}/);
assert.match(portable, /<CanonicalResultRerunReplay/);
assert.doesNotMatch(workspace, /UNIVERSAL VIEWER|content-addressed artifacts/);
assert.doesNotMatch(workspace, /evidence\.lab\.resultKind|без запуска тяжёлого/);
assert.match(workspace, /Проверяем точную связь результата/);
assert.match(workspace, /role="alert"/);
assert.match(workspace, /Повторить/);
assert.match(workspace, /Закрыть разбор/);
assert.match(
workspace,
/const returnToOverview[\s\S]*closeReplay\(\);[\s\S]*scrollIntoView\(\{ block: "start" \}\)/,
);
assert.match(
workspace,
/<h3>\{replayEvidence && selectedSession[\s\S]*evidenceConfigurationLabel\([\s\S]*replayEvidence,[\s\S]*aiJobsController\.moduleLabelsBySetup,[\s\S]*aiJobsController\.jobs,[\s\S]*\)[\s\S]*replay\.binding\.resultId\}<\/h3>/,
);
assert.doesNotMatch(workspace, /RAVNOVES004TREE · полный маршрут восприятия/);
assert.match(workspace, /const selectSession[\s\S]*closeReplay\(\);[\s\S]*setSelectedSessionId/);
assert.match(workspace, /data-observatory-authority="observation-only"/);
assert.doesNotMatch(workspace, /Нарушена связь|compactIdentity/);
assert.doesNotMatch(
`${workspace}\n${hook}`,
/RerunViewport|ObservationSessionSelect|resolveObservationSessionReplay|resolveCanonicalLabReplay|deleteObservationSession|setInterval|setTimeout|useObservationSessions|useAdvancedLaboratoryCatalog|advanced-index|prefetch|preload/,
);
assert.equal(sharedReplay.match(/<RerunViewport\b/g)?.length, 1);
assert.match(sharedReplay, /recordedSessionRerunProfile/);
assert.match(sharedReplay, /timelineStartSeconds/);
assert.doesNotMatch(sharedReplay, /live-acquisition|lab-recorded-evidence/);
assert.match(
legacyReplayWrapper,
/export \{ CanonicalVegetationRerunReplay \} from "\.\.\/\.\.\/components\/laboratory\/CanonicalVegetationRerunReplay"/,
);
assert.match(recordedRun, /fetchVegetationShadowResultMetadata/);
assert.doesNotMatch(recordedRun, /advanced-index|recording\.rrd|canonical-replay\.rrd/);
assert.doesNotMatch(workspace, /workspaces\/laboratory|LaboratoryRecordedClipPlayer|SimulationViewport/);
assert.match(viewerProfiles, /kind: "live-acquisition"/);
assert.match(viewerProfiles, /kind: "recorded-session"/);
assert.match(viewerProfiles, /kind: "lab-recorded-evidence"/);
assert.match(
styles,
/@container observatory-workspace \(max-width: 920px\)[\s\S]*\.observatory-catalog-bar__controls \.nodedc-select-anchor \{[\s\S]*flex: 0 0 auto;/,
);
assert.match(
styles,
/\.observatory-session-summary \{[\s\S]*grid-template-columns:[\s\S]*\.observatory-evidence-card \{[\s\S]*background: var\(--nodedc-glass-control-bg\);/,
);
assert.match(
styles,
/\.observatory-session-stack \{[\s\S]*container-name: observatory-session;[\s\S]*container-type: inline-size;/,
);
assert.match(
styles,
/\.observatory-workspace \{[\s\S]*grid-auto-rows: max-content;[\s\S]*align-content: start;/,
);
assert.match(
styles,
/\.observatory-session-summary__facts dd \{[\s\S]*font-size: var\(--nodedc-font-size-sm\);/,
);
assert.match(
styles,
/\.observatory-catalog-bar h3,[\s\S]*\.observatory-session-summary h3,[\s\S]*\.observatory-evidence h3 \{[\s\S]*font-size: var\(--nodedc-font-size-lg\);/,
);
assert.match(
styles,
/@container observatory-session \(max-width: 46rem\) \{[\s\S]*\.observatory-session-summary \{[\s\S]*grid-template-columns: minmax\(0, 1fr\);/,
);
assert.match(
styles,
/@container observatory-session \(max-width: 38rem\) \{[\s\S]*\.observatory-session-summary__facts \{[\s\S]*grid-template-columns: minmax\(0, 1fr\);/,
);
assert.doesNotMatch(styles, /\.observatory-evidence-card[\s\S]*background:\s*(?:#0{3,6}|black|rgb\(0[ ,])/i);
assert.doesNotMatch(styles, /nodedc-glass-surface|nodedc-status-badge/);
});
test("Observatory rename and delete use admitted projection mutations and canonical windows", async () => {
const [workspace, hook] = await Promise.all([
read("workspaces/observatory/ObservatoryWorkspace.tsx"),
read("core/observatory/useObservatoryCatalog.ts"),
]);
assert.match(
workspace,
/deleteObservatoryLabProjection,[\s\S]*renameObservatoryLabProjection,[\s\S]*from "\.\.\/\.\.\/core\/observatory\/catalogMutations"/,
);
assert.match(workspace, /<Window[\s\S]*title="Переименовать лабораторный результат"/);
assert.match(workspace, /<TextField[\s\S]*label="Название"[\s\S]*maxLength=\{160\}/);
assert.match(workspace, /<WindowFooterActions>[\s\S]*Сохранить/);
assert.match(workspace, /<ConfirmationModal[\s\S]*title="Удалить результат из Обсерватории\?"/);
assert.match(workspace, /Исходная сессия, запечатанный лабораторный результат и файлы доказательств/);
assert.match(workspace, /renameAICompositionRunProjection/);
assert.match(workspace, /deleteAICompositionRunProjection/);
assert.match(workspace, /openCompositionRename\(run\)/);
assert.match(workspace, /openCompositionDelete\(run\)/);
assert.match(
workspace,
/const result = await renameObservatoryLabProjection\([\s\S]*controller\.applyEvidenceRename\(result\.sessionId, result\.displayName\);[\s\S]*setRenameTarget\(null\);[\s\S]*void controller\.refresh\(\);/,
);
assert.match(
workspace,
/catch \(error\) \{[\s\S]*const reconciled = await controller\.refresh\(\);[\s\S]*evidence\?\.label === reconciliation\.displayName[\s\S]*setRenameTarget\(null\);/,
);
const deleteFlowStart = workspace.indexOf("const confirmDelete = useCallback");
const deleteFlowEnd = workspace.indexOf("}, [aiJobsController, closeReplay", deleteFlowStart);
assert.ok(deleteFlowStart >= 0 && deleteFlowEnd > deleteFlowStart);
const wholeDeleteFlow = workspace.slice(deleteFlowStart, deleteFlowEnd);
const evidenceFlowStart = wholeDeleteFlow.indexOf("if (!deleteTarget?.recordedRun");
assert.ok(evidenceFlowStart >= 0);
const deleteFlow = wholeDeleteFlow.slice(evidenceFlowStart);
const teardown = deleteFlow.indexOf("closeReplay();");
const remove = deleteFlow.indexOf("await deleteObservatoryLabProjection");
const tombstone = deleteFlow.indexOf("controller.applyEvidenceDeletion");
const refresh = deleteFlow.indexOf("void controller.refresh();");
assert.ok(teardown >= 0 && teardown < remove, "active replay must unmount before projection delete");
assert.ok(remove < tombstone, "local tombstone must follow the exact empty 204");
assert.ok(tombstone < refresh, "reconciliation refresh must follow the local tombstone");
assert.match(deleteFlow, /flushSync\(\(\) => \{[\s\S]*closeReplay\(\);[\s\S]*\}\);/);
assert.match(
deleteFlow,
/catch \(error\) \{[\s\S]*const reconciled = await controller\.refresh\(\);[\s\S]*observatoryCatalogConfirmsEvidenceDeletion\([\s\S]*setDeleteTarget\(null\);/,
);
assert.match(workspace, /mutationError[\s\S]*role="alert"/);
assert.match(hook, /activeRequestRef\.current\?\.controller\.abort\(\)/);
assert.match(hook, /requestSequenceRef\.current !== id/);
assert.match(hook, /const requestMutationRevision = mutationRevisionRef\.current/);
assert.match(hook, /reconcileObservatoryCatalogMutationOverlay\(/);
assert.match(hook, /applyObservatoryCatalogMutationOverlay\(/);
});
test("Observatory configures independent AI modules and shows queued evidence progress", async () => {
const [workspace, styles, configWindow, jobsHook, sharedReplay] = await Promise.all([
read("workspaces/observatory/ObservatoryWorkspace.tsx"),
read("styles/observatory.css"),
read("components/observatory/AIConfigurationWindow.tsx"),
read("core/observatory/useAICompositionJobs.ts"),
read("components/laboratory/CanonicalResultRerunReplay.tsx"),
]);
assert.match(workspace, /className="observatory-catalog-bar" padding="sm"/);
assert.match(workspace, /label="Выбрать сохранённую сессию"/);
assert.match(workspace, />\s*Сконфигурировать AI-слой\s*<\/Button>/);
assert.doesNotMatch(workspace, /label="Выбрать сетап лаборатории"/);
assert.doesNotMatch(workspace, />\s*Обновить\s*<\/Button>/);
assert.doesNotMatch(workspace, /Выбор меняет только читаемую карточку/);
assert.doesNotMatch(workspace, /ObservatorySetupDetail|observatory-setup-detail/);
assert.doesNotMatch(styles, /observatory-setup-detail|observatory-setup-results/);
assert.doesNotMatch(workspace, /useObservatoryRecordedJobs|useObservatoryLaboratorySetups/);
assert.match(configWindow, /title="Сконфигурировать AI-слой"/);
assert.match(configWindow, /label: "Сегментация"/);
assert.match(configWindow, /Зависимые блоки подключаются явно/);
assert.match(configWindow, /label: "Облако точек \/ TGS"/);
assert.match(configWindow, /label: "Дистанция"/);
assert.match(configWindow, /Дистанция использует рамки детектора и синхронное облако точек/);
assert.match(configWindow, /TGS обрабатывает LiDAR независимо от сегментации и детектора/);
assert.match(configWindow, /providers\.length === 1/);
assert.match(
configWindow,
/<WindowFooterActions>[\s\S]*"Отправляем на Worker…" : "Рассчитать"/,
);
assert.match(configWindow, /className=\{state === "saving" \? "observatory-ai-config__calculate--saving"/);
assert.match(configWindow, /aria-busy=\{state === "saving"\}/);
assert.match(styles, /@keyframes observatory-ai-calculate-pulse/);
assert.match(
styles,
/\.observatory-ai-config__calculate--saving:disabled \{[\s\S]*animation: observatory-ai-calculate-pulse 1\.8s ease-in-out infinite;[\s\S]*opacity: 1;/,
);
assert.match(workspace, /useAICompositionJobs\(selectedSessionId\)/);
assert.match(workspace, /observatory-evidence-card--progress/);
assert.match(workspace, /const active = run\.state === "running"/);
assert.match(workspace, /\{active \? \([\s\S]*observatory-evidence-card__progress/);
assert.match(workspace, /run\.state === "ready" \? <Icon name="clipboard"/);
assert.match(workspace, /function aiJobStage\(/);
assert.match(workspace, /return "Инициализация"/);
assert.match(workspace, /return "Передача на сервер"/);
assert.match(workspace, /return "Просчёт"/);
assert.match(workspace, /Собираем видеоряд для модели/);
assert.match(workspace, /измеренная скорость Worker 006: 1718 FPS/);
assert.doesNotMatch(workspace, /phaseElapsedSeconds \/ progress\.completed/);
assert.match(workspace, /AI_PROGRESS_PHASE_RANGES/);
assert.match(workspace, /"source-preparation": \[5, 25\]/);
assert.match(workspace, /computing: \[25, 90\]/);
assert.match(workspace, /Подготавливаем камеру и LiDAR/);
assert.match(workspace, /прошло.*formatDuration\(progress\.elapsedSeconds\)/s);
assert.match(workspace, /Не удалось запустить модель/);
assert.doesNotMatch(workspace, /job\.terminalMessage|job\.publication\.error/);
assert.doesNotMatch(workspace, /function aiModuleLabel\(/);
assert.match(workspace, /aiJobsController\.moduleLabelsBySetup\[job\.setupId\] \?\? job\.setupId/);
assert.match(jobsHook, /fetchAIModuleCatalog\(request\.signal\)/);
assert.match(jobsHook, /AI_MODULE_SETUP_IDS\[module\.moduleId\]/);
assert.match(jobsHook, /run\.presentation\.modules\.flatMap/);
assert.match(jobsHook, /Object\.fromEntries\(\[\.\.\.catalogLabels, \.\.\.projectedLabels\]\)/);
assert.match(workspace, /<strong>\{moduleLabel\}<\/strong>/);
assert.match(workspace, /<strong>\{run\.displayName \?\? run\.presentation\.configurationLabel\}<\/strong>/);
assert.doesNotMatch(workspace, /<strong>\{selectedSession\.source\.label\} ·/);
assert.match(workspace, /type EvidenceSortDirection = "newest" \| "oldest"/);
assert.match(workspace, /const presentedRows = useMemo<readonly ObservatoryEvidenceRow\[]>/);
assert.match(workspace, /direction \* \(Date\.parse\(left\.timestamp\) - Date\.parse\(right\.timestamp\)\)/);
assert.match(workspace, /className="observatory-evidence__sort"/);
assert.match(workspace, /data-direction=\{evidenceSortDirection\}/);
assert.match(workspace, /formatTimestamp\(row\.timestamp\)/);
assert.match(styles, /\.observatory-evidence-card\s*\{[\s\S]*grid-template-columns:\s*auto minmax\(0, 1fr\) auto auto/);
assert.match(styles, /\.observatory-evidence__sort\s*\{[\s\S]*border:\s*0;[\s\S]*outline:\s*0/);
assert.match(
styles,
/\.observatory-evidence-card__copy strong \{[\s\S]*font-size: var\(--nodedc-font-size-sm\);/,
);
assert.match(sharedReplay, /LAB_SCENE_SETTINGS_STORAGE_PREFIX/);
assert.match(sharedReplay, /window\.localStorage\.getItem/);
assert.match(sharedReplay, /window\.localStorage\.setItem/);
assert.match(sharedReplay, /unifiedCameraShare: blueprintSplitPrimarySize \/ 100/);
assert.match(sharedReplay, /setSettingsOpen\(false\);[\s\S]*void saveLabViewProfile/);
assert.match(
sharedReplay,
/showPoints: spatialMode !== null && \(showSourcePoints \|\| showLocalSlam\)/,
);
assert.match(sharedReplay, /showTrajectory: spatialMode !== null && showLocalSlam/);
assert.match(
sharedReplay,
/accumulationSeconds: showLocalSlam \? sceneDraft\.accumulationSeconds : 0/,
);
assert.match(workspace, /<AICompositionReplay run=\{replay\.run\} \/>/);
assert.match(workspace, /function presentAIJob\(job: ObservatoryRecordedJob\)/);
assert.match(workspace, /if \(job\.state === "failed"\) return true/);
assert.match(workspace, /return job\.state !== "succeeded"/);
assert.match(workspace, /function presentLatestAIJobs/);
assert.match(workspace, /seenSetups\.has\(job\.setupId\)/);
assert.match(configWindow, /fetchAICompositionJobs\(sourceSessionId, request\.signal\)/);
assert.match(
configWindow,
/if \(job\.state === "failed" \|\| job\.publication\.state === "failed"\) continue;/,
);
assert.match(configWindow, /Текущая конфигурация уже рассчитана/);
assert.match(configWindow, /configurationAlreadyExists/);
assert.match(configWindow, /Готовый результат будет использован без повторного расчёта/);
assert.doesNotMatch(configWindow, /disabled: existingByModule\.has\(module\.moduleId\)/);
assert.doesNotMatch(configWindow, /selections\.some\(\(\{ module \}\) => existingByModule/);
assert.match(jobsHook, /const OPEN = new Set/);
assert.match(jobsHook, /1_500/);
assert.match(jobsHook, /globalThis\.setTimeout/);
assert.doesNotMatch(jobsHook, /setInterval/);
assert.match(workspace, /job\.publication\.state === "published"/);
assert.match(workspace, /onCalculated=\{\(\) => aiJobsController\.refresh\(\)\}/);
assert.match(
styles,
/\.observatory-catalog-bar__controls \{[\s\S]*min-width: 0;[\s\S]*flex: 1 1 auto;[\s\S]*flex-wrap: nowrap;[\s\S]*justify-content: flex-end;/,
);
assert.match(styles, /\.observatory-evidence-card__progress \{/);
assert.match(styles, /\.observatory-ai-module-group > header \{/);
assert.match(styles, /\.observatory-ai-module-group \{[\s\S]*border: 0;[\s\S]*outline: 0;/);
assert.match(
styles,
/@container observatory-workspace \(max-width: 920px\)[\s\S]*\.observatory-catalog-bar,[\s\S]*\.observatory-catalog-bar__controls,[\s\S]*flex-direction: column;/,
);
});
@@ -324,7 +324,7 @@ test("Polygon exposes one dataset surface and keeps legacy links compatible", ()
assert.equal(workspaceById("datasets").kind, "datasets");
assert.deepEqual(
workspacesForRoot("polygon").map(({ id }) => id),
["lab-archive", "simulations"],
["lab-archive", "simulations", "observatory"],
);
assert.equal(
workspacesForRoot("system").some(({ id }) => id === "polygon-run"),
@@ -27,6 +27,19 @@ test("top navigation has no Center and Park owns contour health first", () => {
);
assert.equal(productModel.workspacesForRoot("fleet")[0]?.id, "contour-health");
assert.equal(productModel.workspaceById("contour-health")?.root, "fleet");
assert.deepEqual(
productModel.workspacesForRoot("polygon").map(({ id }) => id),
["lab-archive", "simulations", "observatory"],
);
assert.equal(productModel.workspaceById("lab-archive")?.kind, "lab-archive");
assert.deepEqual(
{
root: productModel.workspaceById("observatory")?.root,
kind: productModel.workspaceById("observatory")?.kind,
icon: productModel.workspaceById("observatory")?.icon,
},
{ root: "polygon", kind: "observatory", icon: "eye" },
);
});
test("every laboratory result uses the shared evidence template", async () => {
@@ -0,0 +1,101 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server, createRecordedRerunCameraJournal, initialEye;
before(async () => {
server = await createServer({ appType: "custom", logLevel: "silent",
server: { middlewareMode: true } });
const module = await server.ssrLoadModule(
"/src/components/rerun/recordedRerunCameraJournal.ts");
createRecordedRerunCameraJournal = module.createRecordedRerunCameraJournal;
initialEye = module.RECORDED_RERUN_ORBITAL_EYE;
});
after(async () => { await server?.close(); });
class FakeEvent {
constructor(type, init = {}) { this.type = type; Object.assign(this, init); }
}
class FakeTarget {
listeners = new Map();
emitted = [];
addEventListener(type, listener) {
const listeners = this.listeners.get(type) ?? [];
listeners.push(listener); this.listeners.set(type, listeners);
}
removeEventListener(type, listener) {
this.listeners.set(type, (this.listeners.get(type) ?? []).filter(item => item !== listener));
}
dispatchEvent(event) {
this.emitted.push(event);
for (const listener of this.listeners.get(event.type) ?? []) listener(event);
return true;
}
}
const radius = (eye) => Math.hypot(
eye.position[0] - eye.lookTarget[0],
eye.position[1] - eye.lookTarget[1],
eye.position[2] - eye.lookTarget[2],
);
test("recorded orbital eye tracks only 3D viewport navigation", () => {
const scope = new FakeTarget();
const timers = [];
Object.assign(scope, {
WheelEvent: FakeEvent,
requestAnimationFrame(callback) { callback(); return 1; },
setTimeout(callback) { timers.push(callback); return timers.length; },
});
const canvas = new FakeTarget();
Object.assign(canvas, {
clientHeight: 200,
isConnected: true,
getBoundingClientRect: () => ({
left: 100, right: 500, top: 50, bottom: 250, width: 400, height: 200,
}),
});
const journal = createRecordedRerunCameraJournal(canvas, scope);
journal.configure(initialEye, 0.46);
const pointer = (type, x, y, buttons) => new FakeEvent(type, {
clientX: x, clientY: y, button: 0, buttons, pointerId: 7, pointerType: "mouse",
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
});
canvas.dispatchEvent(pointer("pointerdown", 200, 100, 1));
scope.dispatchEvent(pointer("pointermove", 240, 120, 1));
scope.dispatchEvent(pointer("pointerup", 240, 120, 0));
assert.deepEqual(journal.current(), initialEye);
canvas.dispatchEvent(pointer("pointerdown", 380, 100, 1));
scope.dispatchEvent(pointer("pointermove", 420, 120, 1));
scope.dispatchEvent(pointer("pointerup", 420, 120, 0));
const rotated = journal.current();
assert.notDeepEqual(rotated.position, initialEye.position);
assert.ok(Math.abs(radius(rotated) - radius(initialEye)) < 1e-9);
journal.setMaxOrbitalRadius(40);
scope.dispatchEvent(new FakeEvent("wheel", {
clientX: 380, clientY: 130, deltaX: 0, deltaY: 2_000, deltaMode: 0,
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
}));
assert.ok(Math.abs(radius(journal.current()) - 40) < 1e-9);
journal.configure(rotated, 0.46);
scope.dispatchEvent(new FakeEvent("wheel", {
clientX: 0, clientY: 0, deltaX: 0, deltaY: -20, deltaMode: 0,
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
}));
const zoomed = journal.current();
assert.ok(Math.abs(radius(zoomed) - radius(rotated) * Math.exp(-20 / 200)) < 1e-9);
const snapshot = journal.current();
journal.setSpatialViewportStart(0.8);
scope.dispatchEvent(new FakeEvent("wheel", {
clientX: 380, clientY: 130, deltaX: 0, deltaY: -20, deltaMode: 0,
}));
assert.deepEqual(journal.current(), snapshot);
journal.dispose();
});
@@ -0,0 +1,261 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server, createIsolatedRerunHost, keepRecordedBlueprintSession, createRecordedRerunFacade, createRecordedRerunOwner, relayIsolatedRerunInput;
before(async () => {
server = await createServer({ appType: "custom", logLevel: "silent",
server: { middlewareMode: true } });
({ createIsolatedRerunHost } = await server.ssrLoadModule(
"/src/components/rerun/isolatedRerunHost.ts"));
({ keepRecordedBlueprintSession } = await server.ssrLoadModule(
"/src/core/observation/recordedBlueprintLifecycle.ts"));
({ createRecordedRerunFacade } = await server.ssrLoadModule(
"/src/components/rerun/recordedRerunFacade.ts"));
({ createRecordedRerunOwner } = await server.ssrLoadModule(
"/src/components/rerun/recordedRerunOwner.ts"));
({ relayIsolatedRerunInput } = await server.ssrLoadModule(
"/src/components/rerun/isolatedRerunInput.ts"));
});
after(async () => { await server?.close(); });
function fixture(t, { stopFails = false } = {}) {
const timers = new Map();
let timerId = 0, creates = 0, stops = 0, removals = 0;
t.mock.method(globalThis, "setTimeout", (fn) => {
timers.set(++timerId, fn); return timerId;
});
t.mock.method(globalThis, "clearTimeout", (id) => timers.delete(id));
const listeners = new Map();
const viewer = { stop() { stops++; if (stopFails) throw new Error("partial start"); } };
const mount = {};
const frame = {
contentWindow: {
missionCoreRerun: createRecordedRerunOwner(() => { creates++; return viewer; }, mount),
addEventListener: (name, fn) => listeners.set(name, fn),
removeEventListener: (name) => listeners.delete(name),
},
remove() { removals++; },
};
const host = { dataset: {}, ownerDocument: { createElement(name) {
assert.equal(name, "iframe"); return frame;
} }, append(child) { assert.equal(child, frame); } };
return { host, frame, viewer, mount, timers, listeners,
counts: () => ({ creates, stops, removals }) };
}
function bridge(native, mount = {}) {
const child = createRecordedRerunOwner(() => native, mount);
const parent = createRecordedRerunFacade((request, bytes) => {
assert.equal(typeof request, "string");
const result = child.invoke(request, bytes);
assert.equal(typeof result, "string");
return result;
});
child.connect(message => {
assert.equal(typeof message, "string");
parent.notify(message);
});
return parent;
}
test("recorded realm terminates on close even if upstream stop throws", async (t) => {
const f = fixture(t, { stopFails: true });
const scope = createIsolatedRerunHost(f.host);
assert.equal(f.frame.src, "/rerun-runtime.html");
f.frame.onload();
const ready = await scope.ready;
assert.notEqual(ready.viewer, f.viewer);
assert.equal(ready.mount, f.host);
assert.equal(f.timers.size, 0);
assert.equal(f.listeners.size, 1);
scope.dispose(); scope.dispose();
assert.deepEqual(f.counts(), { creates: 1, stops: 1, removals: 1 });
assert.equal(f.listeners.size, 0);
assert.equal(f.frame.onload, null);
assert.equal(ready.viewer.ready, false);
});
test("native frame input relays Escape and leaves pointer ownership inside Rerun", () => {
const listeners = new Map(), events = [];
class ParentEvent { constructor(type, init) { this.type = type; Object.assign(this, init); } }
const host = { dataset: {}, ownerDocument: { defaultView: {
KeyboardEvent: ParentEvent, PointerEvent: ParentEvent,
} }, dispatchEvent: event => events.push(event) };
const child = {
addEventListener: (name, fn) => listeners.set(name, fn),
removeEventListener: name => listeners.delete(name),
};
const release = relayIsolatedRerunInput(host, child, null);
assert.equal(listeners.has("pointerdown"), false);
listeners.get("keydown")({ key: "Escape", defaultPrevented: false });
assert.equal(events[0].key, "Escape");
release(); release();
assert.equal(listeners.size, 0); assert.equal(events.length, 1);
});
test("disposed facade, channel and unsubscribe reject native use and copy event values", async () => {
let eventCallback, unsubscribed = 0, sends = 0, starts = 0;
const range = { min: 1, max: 2 }, mount = {};
const native = {
ready: true, stop() {},
async start(source, parent) { assert.equal(parent, mount); starts++; },
get_time_range: () => range,
on(event, callback) { eventCallback = callback; return () => { unsubscribed++; }; },
open_channel: () => ({ ready: true, send_rrd() { sends++; }, close() {} }),
};
const { facade, dispose } = bridge(native, mount);
await facade.start(null, {}, null);
assert.equal(starts, 1);
let event;
const unsubscribe = facade.on("time_update", value => { event = value; });
eventCallback(range);
assert.deepEqual(event, range); assert.notEqual(event, range);
assert.notEqual(facade.get_time_range(), range);
const channel = facade.open_channel();
channel.send_rrd(new Uint8Array());
dispose(); dispose(); unsubscribe();
assert.equal(unsubscribed, 1);
assert.equal(channel.ready, false);
channel.send_rrd(new Uint8Array());
eventCallback({ min: 3, max: 4 });
assert.deepEqual(event, range);
assert.equal(sends, 1);
await assert.rejects(facade.start(null, {}, null), /disposed/);
});
test("close before frame load cancels startup and cannot reopen", async (t) => {
const f = fixture(t);
const scope = createIsolatedRerunHost(f.host);
const lateLoad = f.frame.onload;
const rejected = assert.rejects(scope.ready, /disposed/);
scope.dispose(); lateLoad();
await rejected;
assert.deepEqual(f.counts(), { creates: 0, stops: 0, removals: 1 });
assert.equal(f.timers.size, 0);
});
test("closing during SDK startup also releases a late successful start", async () => {
let finish, stops = 0;
const owner = bridge({
start: () => new Promise(resolve => { finish = resolve; }),
stop() { stops++; },
}, {});
const pending = owner.facade.start(null, {}, null);
const rejected = assert.rejects(pending, /disposed/);
owner.dispose();
assert.equal(stops, 1);
finish();
await rejected;
await Promise.resolve();
assert.equal(stops, 2);
assert.equal(owner.facade.ready, false);
});
test("late failed startup cannot revive a disposed parent and still finishes cleanup", async () => {
let fail, stops = 0;
const owner = bridge({
start: () => new Promise((_resolve, reject) => { fail = reject; }),
stop() { stops++; if (stops > 1) throw new Error("already gone"); },
}, {});
const pending = owner.facade.start(null, {}, null);
const rejected = assert.rejects(pending, /disposed/);
owner.dispose();
fail(new Error("startup failed"));
await rejected;
await Promise.resolve();
assert.equal(stops, 2);
assert.equal(owner.facade.ready, false);
});
test("primitive bridge preserves all public clock/control arguments and errors", async () => {
const calls = [];
const methods = {
open: [["one.rrd", "two.rrd"]], close: ["one.rrd"],
override_panel_state: ["time", "hidden"],
get_active_recording_id: [], get_active_timeline: ["recording"],
get_current_time: ["recording", "session_time"], get_playing: ["recording"],
get_time_range: ["recording", "session_time"],
set_active_timeline: ["recording", "session_time"],
set_current_time: ["recording", "session_time", 123.456],
set_playing: ["recording", false],
};
const native = { stop() {}, start: async () => { throw new Error("native startup failure"); } };
for (const name of Object.keys(methods)) native[name] = (...args) => { calls.push([name, args]); return 1; };
const { facade, dispose } = bridge(native);
for (const [name, args] of Object.entries(methods)) facade[name](...args);
assert.deepEqual(calls, Object.entries(methods));
await assert.rejects(facade.start(null, {}, null), /native startup failure/);
dispose();
});
test("sync native failures become parent-owned errors and optional channel names stay undefined", () => {
const nativeError = new Error("native failure");
let channelName = "unobserved";
const { facade, dispose } = bridge({
stop() {},
open() { throw nativeError; },
open_channel(name) { channelName = name; return { close() {} }; },
});
assert.throws(() => facade.open("rrd"), error => error !== nativeError && /native failure/.test(error.message));
facade.open_channel();
assert.equal(channelName, undefined);
dispose();
});
test("dispose closes every auxiliary channel even if one channel fails", () => {
const closed = [];
let stops = 0;
const owner = bridge({
stop() { stops++; },
open_channel: name => ({ close() {
closed.push(name);
if (name === "first") throw new Error("already gone");
} }),
}, {});
const first = owner.facade.open_channel("first");
const second = owner.facade.open_channel("second");
owner.dispose(); owner.dispose(); first.close(); second.close();
assert.deepEqual(closed, ["first", "second"]);
assert.equal(stops, 1);
});
test("host load timeout removes the frame and clears its timer", async (t) => {
const f = fixture(t);
const scope = createIsolatedRerunHost(f.host);
const rejected = assert.rejects(scope.ready, /timed out/);
[...f.timers.values()][0]();
await rejected;
assert.equal(f.counts().removals, 1);
assert.equal(f.timers.size, 0);
});
test("blueprint termination cancels renewal and sends one bounded keepalive release", async () => {
let tick, canceled = 0;
const requests = [];
const release = keepRecordedBlueprintSession({
endpointUrl: "/api/v1/observation-sessions/source/blueprint.rrd",
origin: "http://core.test", applicationId: "nodedc_mission_core_recorded",
recordingId: "recording", ownerId: "a".repeat(32),
fetcher: async (url, init) => { requests.push({ url, ...init }); return {}; },
schedule(fn) { tick = fn; return 1; }, cancel() { canceled++; },
});
tick();
const renewal = requests[0];
release(); release(); tick();
assert.equal(canceled, 1);
assert.equal(requests.length, 2);
assert.equal(renewal.signal.aborted, true);
assert.equal(requests[1].keepalive, true);
assert.equal(requests[1].url, "http://core.test/api/v1/observation-sessions/source/blueprint-lifecycle");
assert.deepEqual(JSON.parse(requests[1].body), {
action: "release", application_id: "nodedc_mission_core_recorded",
recording_id: "recording", blueprint_session_id: "a".repeat(32),
});
});
test("blueprint lease refuses a foreign endpoint", () => {
assert.throws(() => keepRecordedBlueprintSession({ endpointUrl: "https://other.test/blueprint.rrd",
origin: "http://core.test", applicationId: "app", recordingId: "recording", ownerId: "owner",
}), /Unsafe/);
});
@@ -14,6 +14,8 @@ let liveRerunReceiverBindingIdentity;
let recordedOpenWatchdogTimeoutMs;
let rerunViewerInitialSource;
let resolveRecordedViewerSourceUrl;
let isRecordedRrdSource;
let resolveRecordedBlueprintUrl;
before(async () => {
server = await createServer({
@@ -31,6 +33,8 @@ before(async () => {
recordedOpenWatchdogTimeoutMs,
rerunViewerInitialSource,
resolveRecordedViewerSourceUrl,
isRecordedRrdSource,
resolveRecordedBlueprintUrl,
} = await server.ssrLoadModule("/src/components/RerunViewport.tsx"));
});
@@ -86,6 +90,25 @@ test("one canonical LAB replay generation reaches the same native receiver", ()
);
});
for (const prefix of [
"m49-tgs-portable-review",
"lab-v1-eomt-ddrnet",
"ai-layer-ddrnet",
"ai-layer-eomt",
"ai-layer-rf-detr",
"ai-layer-object-distance",
]) test(`${prefix} replay uses recorded admission and the shared source blueprint`, () => {
const source = `/api/v1/observatory/portable-results/${prefix}-${"c".repeat(64)}/replays/${"d".repeat(64)}/recording.rrd`;
const blueprint = "/api/v1/observation-sessions/source/blueprint.rrd";
assert.equal(isRecordedRrdSource(source), true);
assert.equal(isRecordedRrdSource(source.replace("/replays/", "/untrusted/")), false);
assert.equal(resolveRecordedViewerSourceUrl({ sourceUrl: source,
viewerSourceUrl: `${source}?generation=${sha256}`, sha256, byteLength: 100 },
"http://mission-core.test"), `http://mission-core.test${source}?generation=${sha256}`);
assert.equal(resolveRecordedBlueprintUrl(source, "http://mission-core.test", blueprint),
`http://mission-core.test${blueprint}`);
});
test("live presentation waits for the exact receiver to expose a usable range", () => {
assert.equal(isLiveRerunPresentationReady(false, { min: 1, max: 2 }, 1), false);
assert.equal(isLiveRerunPresentationReady(true, null, 1), false);
@@ -225,6 +248,9 @@ test("recorded RRD bytes are never split across LogChannel.send_rrd calls", asyn
assert.doesNotMatch(source, /streamVerifiedRecordedRrd/);
assert.doesNotMatch(source, /missioncore\/recorded-recording/);
assert.doesNotMatch(source, /recordedChannel/);
assert.match(source, /recordedPerceptionLayers\.costmap,/);
assert.match(source, /recordedPerceptionLayers\.costmap !== undefined && status !== "ready"/);
assert.match(source, /perceptionLayers\.costmap === undefined \? \{\} : \{\s*show_costmap: perceptionLayers\.costmap,\s*reactivate_updates: true/s);
assert.match(source, /viewer\.start\(\s*rerunViewerInitialSource\(resolvedSource\)/s);
assert.doesNotMatch(source, /rerunViewerOpenOptions/);
assert.match(
@@ -97,7 +97,7 @@ test("M4 keeps independent semantic controls in media and spatial panes", async
assert.match(source, /aria-label="Слои 3D и плана"/);
assert.match(canonical, /data-pane-mode="media"/);
assert.match(canonical, /data-pane-mode="spatial"/);
assert.match(canonical, /modeControlsVisible=\{!splitView\}/);
assert.match(canonical, /modeControlsVisible=\{!splitView && !paneToolbarsAlwaysVisible\}/);
assert.match(source, /semanticOverlay=\{mediaMode === "video" \? semanticOverlay : undefined\}/);
});
@@ -124,6 +124,11 @@ test("Compute-contour telemetry remains a bounded system feature slice", async (
assert.match(pollIntervalContract, /MIN_TELEMETRY_POLL_INTERVAL_SECONDS\s*=\s*1/);
assert.match(telemetryPolling, /normalizeWorkerTelemetryPollMilliseconds/);
assert.match(telemetryPolling, /startSequentialPolling/);
assert.match(telemetryPolling, /catch \(reason: unknown\)[\s\S]*?setTelemetry\(null\)/);
assert.match(computeWorkspace, /sm_clock_mhz/);
assert.match(computeWorkspace, /memory_clock_mhz/);
assert.match(computeWorkspace, /Ожидает восстановления данных/);
assert.doesNotMatch(computeWorkspace, /Очередь свободна; модели остаются загруженными/);
assert.doesNotMatch(telemetryPolling, /if\s*\(\s*!enabled\s*\|\|\s*loading\s*\)/);
assert.match(pollingScheduler, /A transient failure must not stop polling/);
assert.match(styles, /system-telemetry\.css/);
@@ -452,7 +452,7 @@ test("canonical recorded LAB spatial frame keeps source, SLAM and body identity
});
test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival review separate", async () => {
const [resultSource, benchmarkSource, m49Source, canonicalSource, rerunSource] = await Promise.all([
const [resultSource, benchmarkSource, m49Source, canonicalSource, rerunSource, replayStyles] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/VegetationShadowResult.tsx", import.meta.url),
"utf8",
@@ -470,7 +470,11 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
"utf8",
),
readFile(
new URL("../src/workspaces/laboratory/CanonicalVegetationRerunReplay.tsx", import.meta.url),
new URL("../src/components/laboratory/CanonicalResultRerunReplay.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/styles/m4-replay-threat.css", import.meta.url),
"utf8",
),
]);
@@ -490,16 +494,40 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
assert.match(rerunSource, /<RerunViewport/);
assert.match(rerunSource, /ИСХ\. ТОЧКИ/);
assert.match(rerunSource, /ЛОК\. SLAM/);
assert.match(rerunSource, /resolveCanonicalLabReplay/);
assert.match(rerunSource, /resolveReplay\(resultId, value/);
assert.doesNotMatch(rerunSource, /canonical-overlay\.rrd/);
assert.match(rerunSource, /unifiedPerception: splitView/);
assert.match(rerunSource, /lockPerceptionCameraInteraction: false/);
assert.match(rerunSource, /&& !semantics[\s\S]*&& !detections[\s\S]*&& !costmap/);
assert.doesNotMatch(rerunSource, /trackNativeSplit|rerunNativeCursor|rerunNativeInput/);
assert.match(canonicalSource, /resizable=\{splitView\}/);
assert.match(rerunSource, /unifiedCameraShare: blueprintSplitPrimarySize \/ 100/);
assert.match(rerunSource, /data-split-view=\{splitView \? "true" : undefined\}/);
assert.match(rerunSource, /max=\{500\}/);
assert.match(rerunSource, /exactValueBounds=\{\{ min: 0 \}\}/);
assert.match(replayStyles, /m4-replay-threat-visual__pane-toolbar \{[\s\S]*flex-wrap: wrap;/);
assert.match(replayStyles, /m4-replay-threat-visual__spatial-toolbar-end \{[\s\S]*display: contents;/);
assert.match(rerunSource, /<ToastStack items=\{toasts\}/);
assert.match(rerunSource, /isRecordedPlaybackPresentationReady\(viewerStatus, playback\)/);
assert.match(rerunSource, /data-presentation-state=\{presentationState\}/);
assert.match(rerunSource, /<ActivityIndicator label="Загружаем синхронизированную запись"/);
assert.match(rerunSource, /presentationReady && playback && playbackController/);
assert.match(
replayStyles,
/canonical-vegetation-rerun-replay:not\(\[data-presentation-state="ready"\]\)[\s\S]*laboratory-evidence-viewer__controls/,
);
assert.match(
replayStyles,
/canonical-vegetation-rerun-replay__timeline \.observation-timeline__playback \{[\s\S]*grid-template-columns: auto auto minmax\(0, 1fr\) auto;/,
);
assert.doesNotMatch(rerunSource, /LaboratoryRecordedClipPlayer|LaboratoryMetricEvidenceScene/);
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
assert.match(rerunSource, /value: "tgs", label: "TGS", disabled: true/);
assert.match(rerunSource, /hasTgs \? toggle\("TGS", showTgs/);
assert.match(canonicalSource, /primary=\{mediaPane\}/);
assert.match(canonicalSource, /secondary=\{spatialPane/);
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
assert.match(canonicalSource, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
assert.match(canonicalSource, /resizable=\{splitView\}/);
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
assert.match(resultSource, /linkedTgsResultId/);
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
+6
View File
@@ -91,6 +91,12 @@ export default defineConfig(({ mode }) => {
},
build: {
target: "esnext",
rollupOptions: {
input: {
app: fileURLToPath(new URL("./index.html", import.meta.url)),
rerun: fileURLToPath(new URL("./rerun-runtime.html", import.meta.url)),
},
},
},
server: {
host: "127.0.0.1",
+161
View File
@@ -0,0 +1,161 @@
{
"schema_version": "missioncore.observatory-ai-module-registry/v1",
"modules": [
{
"module_id": "camera-source",
"label": "Подготовка камеры K1",
"group": "preparation",
"image_sha256": "da926459aee0a841bbdfaf80a0eb5fbead354c56794d1f3384eeba66d0a49e00",
"implementation_sha256": "a0b1a74887e5f1cf78e867f81f246bae08e872fed320a2939986c17913418524",
"model_sha256": null,
"contract_sha256": "87c9e8864f9469719e69bfd750c418e077944a6260ffc6fd3ce12daf4a1da7d5",
"requires": [
"source.camera"
],
"provides": [
"camera.frames"
],
"optional_inputs": [],
"parameter_choices": {
"cadence": [
1
]
},
"defaults": {
"cadence": 1
},
"state_policy": "stateless"
},
{
"module_id": "ddrnet",
"label": "DDRNet-39 · GOOSE",
"group": "segmentation",
"image_sha256": "489fc7d1157fd0f1cd1d82e06a15737b7b2aaaf72b2ddb2aca2992b91a97488e",
"implementation_sha256": "9a3fceb65374ccc3dccdbe02691c2b62376ff3a3a6d16439cb9f6266e73c78b0",
"model_sha256": "b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6",
"contract_sha256": "197c9d7dd0f55f79992215a57e450c5442f3d1510d74083bcf123577f85da199",
"requires": [
"camera.frames"
],
"provides": [
"segmentation.mask"
],
"optional_inputs": [],
"parameter_choices": {
"cadence": [
1
]
},
"defaults": {
"cadence": 1
},
"state_policy": "stateless"
},
{
"module_id": "eomt",
"label": "EoMT Large · Cityscapes",
"group": "segmentation",
"image_sha256": "5b770178e4a5c8fbe8f8ddab3b83a598973dbe103b669851110b11a0918ab846",
"implementation_sha256": "17ac471d5c51f70b3af6fb6046e1880c731691253c338b1626702ce4ca375d2b",
"model_sha256": "c265da9a74f58f5c3f4826d23ca4ca78beac0b106cca5842beca61580de5b782",
"contract_sha256": "09ca841400b08af0607d19d346f78ab768567256c8c58789a6ade4ede8f425a0",
"requires": [
"camera.frames"
],
"provides": [
"segmentation.mask"
],
"optional_inputs": [],
"parameter_choices": {
"cadence": [
1
]
},
"defaults": {
"cadence": 1
},
"state_policy": "stateless"
},
{
"module_id": "tgs",
"label": "TRAVEL TGS · облако и карта проходимости",
"group": "geometry",
"image_sha256": "f9278ab21aa65045be993dd19bffc25f49955e19598893ac78cc4761ca63ecf3",
"implementation_sha256": "52813392aabd02efc5c2b8f7c22ed88e3ef4cc8ad3aafeba2792efe503e29fe9",
"model_sha256": null,
"contract_sha256": "9dd80c8e2504559d2156fca933de6eb27901e35305e6853aeb84707e1cb13892",
"requires": [
"source.calibration",
"source.lidar",
"source.pose"
],
"provides": [
"geometry.costmap",
"geometry.ground"
],
"optional_inputs": [],
"parameter_choices": {
"history-seconds": [
1
]
},
"defaults": {
"history-seconds": 1
},
"state_policy": "causal-reset-at-source-start"
},
{
"module_id": "rf-detr",
"label": "RF-DETR Large · рамки объектов",
"group": "detection",
"image_sha256": "2b8b44be8e9ee4060aa6997fc4c025ad7120f37ecd720a9e59b5e02ac6c90f66",
"implementation_sha256": "4eb580eb938164bf0624e94fdc3b046aacf32fb8022c93c67fa26bab7c3d19d0",
"model_sha256": "b8a40b3580edff001ec9680de68707242294ff590ab296000fae371f1083f695",
"contract_sha256": "815d0afd24355da3a462e0423dab8284165c89cebd136d7c8584a32602178b96",
"requires": [
"camera.frames"
],
"provides": [
"detections.2d"
],
"optional_inputs": [],
"parameter_choices": {
"cadence": [
1
]
},
"defaults": {
"cadence": 1
},
"state_policy": "stateless"
},
{
"module_id": "object-distance",
"label": "Дистанция до объектов · K1 LiDAR",
"group": "range",
"image_sha256": "69d68f64981b41e5bcce1e642433e3d466b88c51942a5ba3aec9180d8ed04263",
"implementation_sha256": "0e3e426e2f768bc019a3715a50fc77a88626869abd9fa919e1aef5763d0c53da",
"model_sha256": null,
"contract_sha256": "5a6e446f009420352587f83388331b368d00950ab2fb896c6350aaa9b31c2e2e",
"requires": [
"detections.2d",
"source.calibration",
"source.lidar",
"source.pose"
],
"provides": [
"objects.ranged"
],
"optional_inputs": [],
"parameter_choices": {
"cadence": [
1
]
},
"defaults": {
"cadence": 1
},
"state_policy": "stateless"
}
]
}
+336
View File
@@ -0,0 +1,336 @@
{
"schema_version": "missioncore.observatory-domain-ontology/v1",
"ontology_id": "mission-core.observatory",
"version": "1.3.0",
"owner": "NODE.DC Mission Core",
"lifecycle": "local-runtime-canonical",
"entities": [
{
"id": "mission.transport-unit",
"identity": "transport_id",
"owner": "transport registry",
"lifecycle": "registered-to-retired"
},
{
"id": "mission.equipment-unit",
"identity": "equipment_id",
"owner": "equipment registry",
"lifecycle": "registered-to-retired"
},
{
"id": "observatory.equipment-mount",
"identity": "mount_id",
"owner": "equipment capture registry",
"lifecycle": "time-bounded"
},
{
"id": "observatory.capture-profile",
"identity": "capture_profile_id",
"owner": "equipment capture registry",
"lifecycle": "versioned"
},
{
"id": "observatory.recorded-session",
"identity": "source_session_id",
"owner": "session archive",
"lifecycle": "recording-to-immutable"
},
{
"id": "observatory.module-version",
"identity": "module_sha256",
"owner": "module registry",
"lifecycle": "installed-or-retired"
},
{
"id": "observatory.container-image",
"identity": "image_sha256",
"owner": "module registry",
"lifecycle": "built-to-retired"
},
{
"id": "observatory.worker-node",
"identity": "worker_node_id",
"owner": "worker registry",
"lifecycle": "registered-to-retired"
},
{
"id": "observatory.composition",
"identity": "composition_sha256",
"owner": "composition store",
"lifecycle": "immutable"
},
{
"id": "observatory.composition-run",
"identity": "run_id",
"owner": "composition run store",
"lifecycle": "append-only"
},
{
"id": "observatory.recorded-job",
"identity": "job_id",
"owner": "recorded job queue",
"lifecycle": "queued-to-terminal"
},
{
"id": "observatory.portable-result",
"identity": "result_id",
"owner": "portable result publisher",
"lifecycle": "immutable"
},
{
"id": "observatory.lab-projection",
"identity": "result_id",
"owner": "session catalog",
"lifecycle": "published"
},
{
"id": "observatory.lab-view-profile",
"identity": "result_id",
"owner": "replay presentation",
"lifecycle": "mutable-per-result"
},
{
"id": "observatory.viewer-pane",
"identity": "pane_id",
"owner": "replay presentation",
"lifecycle": "versioned-contract"
},
{
"id": "observatory.viewer-layer",
"identity": "layer_id",
"owner": "replay presentation",
"lifecycle": "versioned-contract"
}
],
"relations": [
{
"id": "observatory.equipment-mount.mounts_equipment",
"from": "observatory.equipment-mount",
"to": "mission.equipment-unit",
"cardinality": "many-to-one"
},
{
"id": "observatory.equipment-mount.attaches_to_transport",
"from": "observatory.equipment-mount",
"to": "mission.transport-unit",
"cardinality": "many-to-one"
},
{
"id": "observatory.recorded-session.captured_on_transport",
"from": "observatory.recorded-session",
"to": "mission.transport-unit",
"cardinality": "many-to-zero-or-one"
},
{
"id": "observatory.recorded-session.captured_with_equipment",
"from": "observatory.recorded-session",
"to": "mission.equipment-unit",
"cardinality": "many-to-many"
},
{
"id": "observatory.recorded-session.uses_equipment_mount",
"from": "observatory.recorded-session",
"to": "observatory.equipment-mount",
"cardinality": "many-to-many"
},
{
"id": "observatory.recorded-session.uses_capture_profile",
"from": "observatory.recorded-session",
"to": "observatory.capture-profile",
"cardinality": "many-to-one"
},
{
"id": "observatory.composition-run.uses_recorded_session",
"from": "observatory.composition-run",
"to": "observatory.recorded-session",
"cardinality": "many-to-one"
},
{
"id": "observatory.composition.contains_module",
"from": "observatory.composition",
"to": "observatory.module-version",
"cardinality": "one-to-many"
},
{
"id": "observatory.module-version.implemented_by_image",
"from": "observatory.module-version",
"to": "observatory.container-image",
"cardinality": "many-to-one"
},
{
"id": "observatory.container-image.installed_on_worker",
"from": "observatory.container-image",
"to": "observatory.worker-node",
"cardinality": "many-to-many"
},
{
"id": "observatory.composition-run.applies_composition",
"from": "observatory.composition-run",
"to": "observatory.composition",
"cardinality": "many-to-one"
},
{
"id": "observatory.composition-run.uses_job",
"from": "observatory.composition-run",
"to": "observatory.recorded-job",
"cardinality": "one-to-many"
},
{
"id": "observatory.recorded-job.executes_on_worker",
"from": "observatory.recorded-job",
"to": "observatory.worker-node",
"cardinality": "many-to-one"
},
{
"id": "observatory.recorded-job.publishes_result",
"from": "observatory.recorded-job",
"to": "observatory.portable-result",
"cardinality": "one-to-zero-or-one"
},
{
"id": "observatory.composition-run.projects_lab",
"from": "observatory.composition-run",
"to": "observatory.lab-projection",
"cardinality": "one-to-zero-or-one"
},
{
"id": "observatory.lab-projection.has_view_profile",
"from": "observatory.lab-projection",
"to": "observatory.lab-view-profile",
"cardinality": "one-to-zero-or-one"
},
{
"id": "observatory.module-version.exposes_layer",
"from": "observatory.module-version",
"to": "observatory.viewer-layer",
"cardinality": "many-to-many"
},
{
"id": "observatory.viewer-layer.belongs_to_pane",
"from": "observatory.viewer-layer",
"to": "observatory.viewer-pane",
"cardinality": "many-to-one"
}
],
"panes": [
{
"pane_id": "camera",
"label": "Камера",
"order": 10
},
{
"pane_id": "spatial",
"label": "Облако точек",
"order": 20
}
],
"layers": [
{
"layer_id": "camera.source",
"pane_id": "camera",
"label": "КАМЕРА",
"control": "toggle",
"order": 10
},
{
"layer_id": "camera.ddrnet",
"pane_id": "camera",
"label": "DDRNET",
"control": "toggle",
"order": 20
},
{
"layer_id": "camera.eomt",
"pane_id": "camera",
"label": "EOMT",
"control": "toggle",
"order": 30
},
{
"layer_id": "camera.detections",
"pane_id": "camera",
"label": "РАМКИ",
"control": "toggle",
"order": 40
},
{
"layer_id": "spatial.source-points",
"pane_id": "spatial",
"label": "ИСХ. ТОЧКИ",
"control": "toggle",
"order": 10
},
{
"layer_id": "spatial.local-slam",
"pane_id": "spatial",
"label": "ЛОК. SLAM",
"control": "toggle-with-settings",
"order": 20
},
{
"layer_id": "spatial.tgs",
"pane_id": "spatial",
"label": "TGS",
"control": "toggle",
"order": 30
}
],
"module_projections": [
{
"module_id": "ddrnet",
"configuration_label": "DDRNet-39 · GOOSE",
"layers": [
"camera.source",
"camera.ddrnet"
]
},
{
"module_id": "eomt",
"configuration_label": "EoMT Large · Cityscapes",
"layers": [
"camera.source",
"camera.eomt"
]
},
{
"module_id": "rf-detr",
"configuration_label": "RF-DETR Large",
"layers": [
"camera.source",
"camera.detections"
]
},
{
"module_id": "tgs",
"configuration_label": "TRAVEL TGS",
"layers": [
"spatial.source-points",
"spatial.local-slam",
"spatial.tgs"
]
},
{
"module_id": "object-distance",
"configuration_label": "Дистанция до объектов · K1 LiDAR",
"layers": [
"camera.source",
"camera.detections",
"spatial.source-points",
"spatial.local-slam"
]
}
],
"named_queries": [
"recording.capture-context",
"composition.configuration-label",
"composition.member-results",
"composition.viewer-layers",
"composition.ready-state",
"lab.view-profile"
],
"platform_sync": {
"mode": "none",
"promotion_gate": "stable cross-product meaning with an agreed Platform Ontology migration",
"platform_repository_is_runtime_dependency": false
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"schema_version": "missioncore.equipment-model-registry/v1",
"models": [
{
"schema_version": "missioncore.equipment-model/v1",
"equipment_model_id": "xgrids.lixelkity-k1",
"equipment_model_version": 1,
"plugin_id": "nodedc.device.xgrids-lixelkity-k1",
"vendor": "XGRIDS",
"display_name": "XGRIDS LixelKity K1",
"category": "mobile-lidar-scanner",
"capability_ids": [
"recorded.camera.video",
"recorded.lidar.point-cloud",
"recorded.pose.trajectory"
],
"authority": {
"commands_enabled": false
}
}
]
}
+97
View File
@@ -0,0 +1,97 @@
{
"schema_version": "missioncore.observatory-laboratory-setup-registry/v1",
"setups": [
{
"setup_id": "m49-tgs-full-shadow-v1",
"display_name": "M4.9T5 · TRAVEL TGS · CPU-only, без ML",
"description": "Сохранённая конфигурация RAVNOVES00: TRAVEL TGS без ML-моделей, 4 489 кадров, causal rolling 1 s, полный визуальный разбор и отдельное доказательство integrated graph.",
"origin": "archived-definition",
"source": {
"session_id": "20260720T065719Z_viewer_live",
"label": "RAVNOVES00",
"required_modalities": [
"point-cloud",
"trajectory",
"video"
]
},
"run_definition": {
"definition_id": "m49-tgs-full-shadow",
"version": 1,
"work_id": "m49-tgs-full-shadow",
"configuration": [
{
"role": "primary-profile",
"path": "config/perception/m49-tgs-full-shadow-v1.json",
"sha256": "c2e07010aaee78259d36c057962d6bfb885349251ff7356d867e5813e632881c"
}
]
},
"executor": {
"contour_id": "worker-006",
"state": "not-installed",
"reason_code": "laboratory-runner-adapter-not-installed",
"reason": "Повторный запуск этого сетапа ещё не подключён к общему контуру расчёта."
},
"preserved_results": [
{
"result_id": "m49-tgs-full-shadow-ef98de7db7596d48e8c8c0549ce68e6704ee03e87c8c4bcf1e3e748b7ccb032e",
"result_kind": "recorded-source-paced-tgs-shadow",
"relation": "primary-visual",
"access": "legacy-lab",
"created_at_utc": "2026-08-26T20:27:19.076865Z"
},
{
"result_id": "m49-tgs-integrated-graph-shadow-75e48fd8acf0246be16613e087fcf26fe909f7834718d217c74785a57f494b24",
"result_kind": "source-paced-integrated-graph-shadow",
"relation": "compute-successor",
"access": "evidence-only",
"created_at_utc": "2026-08-27T08:50:05.5622153+00:00"
}
],
"authority": {
"commands_enabled": false,
"actuation_allowed": false,
"navigation_or_safety_accepted": false,
"production_accepted": false
}
},
{
"setup_id": "lab-v1-ravnoves004tree-final",
"display_name": "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39",
"description": "Точный RAVNOVES004TREE result: 6 830 кадров EoMT Cityscapes Large 1024 + DDRNet-39 и синхронный записанный replay; полный TGS и независимый YOLOX отсутствуют.",
"origin": "existing-result",
"source": {
"session_id": "20260828T130511Z_viewer_live",
"label": "RAVNOVES004TREE",
"required_modalities": [
"point-cloud",
"trajectory",
"video"
]
},
"run_definition": null,
"executor": {
"contour_id": "worker-006",
"state": "not-installed",
"reason_code": "durable-dispatch-definition-unavailable",
"reason": "Для готового результата не сохранён повторно запускаемый сетап; доступен записанный разбор."
},
"preserved_results": [
{
"result_id": "lab-v1-vegetation-shadow-8c8f387599955dd79a16ded2c2d7cfd7f9f308d704f52c42fc26bd39d6da2d60",
"result_kind": "recorded-perception-qualification",
"relation": "canonical-projection",
"access": "observatory",
"created_at_utc": "2026-08-29T18:05:11.329061Z"
}
],
"authority": {
"commands_enabled": false,
"actuation_allowed": false,
"navigation_or_safety_accepted": false,
"production_accepted": false
}
}
]
}
@@ -0,0 +1,61 @@
{
"schema_version": "missioncore.observatory-m49-recorded-queue-binding/v1",
"binding_id": "m49-ravnoves00-recorded-queue-v1",
"source": {
"session_id": "20260720T065719Z_viewer_live",
"label": "RAVNOVES00",
"required_modalities": [
"point-cloud",
"trajectory",
"video"
],
"source_pack": {
"artifact_id": "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b",
"sha256": "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944",
"byte_length": 72996000,
"media_type": "application/vnd.nodedc.lidar-source-pack+npz",
"expected_timeline_frames": 4489,
"expected_available_lidar_frames": 3928
}
},
"setup": {
"setup_id": "m49-tgs-full-shadow-v1",
"definition_id": "m49-tgs-full-shadow",
"definition_version": 1,
"definition_sha256": "836a66639e69f7ea00de2a9111c1c6c9c3c00f1abd726f1df596aeb4e3a88ae6"
},
"source_adapter": {
"adapter_id": "ravnoves00-m49-source-pack",
"version": 1
},
"executor": {
"release_id": "m49-tgs-full-shadow-worker-release",
"artifact_sha256": "5e0ea16c7a5cc760463836718b0cd8b0006ffc4b202e5f706a20a86ef2f912ab",
"code_revision": "40c850b167dda366d8aa45d828520168affaf9fd",
"service_installed": false,
"image_set": {
"schema_version": "missioncore.observatory-executor-image-set/v1",
"images": {
"travel": "7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f",
"parity": "ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0"
}
},
"learned_models": []
},
"resource_profile": {
"schema_version": "missioncore.observatory-recorded-resource-profile/v1",
"profile_id": "worker006-cpu-single-run-v1",
"contour_id": "worker-006",
"concurrency": 1,
"checkpoint_policy": "non-checkpointable",
"restart_from_zero": true,
"staging_discard_required": true,
"resource_release_receipt_required": true
},
"authority": {
"commands_enabled": false,
"actuation_allowed": false,
"navigation_or_safety_accepted": false,
"production_accepted": false
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,43 @@
{
"schema_version": "missioncore.recorded-capture-profile-registry/v1",
"profiles": [
{
"schema_version": "missioncore.recorded-capture-profile/v1",
"capture_profile_id": "xgrids-k1.viewer-live.fw-3.0.2.v1",
"capture_profile_version": 1,
"equipment_model_id": "xgrids.lixelkity-k1",
"equipment_model_sha256": "c95d7183ffd2e425892fd9ddfc4cd8dcbccc0231289127ddc14db6b52bbb92aa",
"plugin_id": "nodedc.device.xgrids-lixelkity-k1",
"archive_id": "xgrids-k1.viewer-live.evidence",
"firmware_compatibility_profile_id": "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
"modalities": [
"point-cloud",
"trajectory",
"video"
],
"semantic_channels": [
"camera.video.recorded",
"spatial.point-cloud.recorded",
"spatial.pose.recorded"
],
"camera_media": {
"source_id": "sensor.camera.right",
"media_type": "video/mp4; codecs=\"avc1.641028\"",
"width": 800,
"height": 600,
"initialization_sha256": "e2279963e16d84c91d68e7dbb1f7efed840533387dfeb844b7398bff45fbde38"
},
"calibration": {
"slot_id": "camera_1",
"sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
},
"media_epoch_policy": "exactly-one-complete-epoch",
"seekable": true,
"adapter": {
"adapter_id": "xgrids-k1-recorded-observatory-v2",
"version": 2,
"adapter_sha256": "4e12be6d2503e2e237eddb290b28d6a7d16cf983855b6d8e6b4e3b65d2feb0de"
}
}
]
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,206 @@
{
"schema_version": "missioncore.perception-profile-candidate/v1",
"profile_id": "k1-perception-ddrnet39-rfdetr-tgs-prototype/v1",
"display_name": "K1 Perception — DDRNet-39 + RF-DETR + TGS",
"status": "stage-1-candidate-not-installed-not-qualified",
"experiment_policy": {
"bounded_lab_overload_allowed": true,
"retain_profiles_that_fail_current_hardware_realtime": true,
"realtime_failure_blocks_experimental_packaging": false,
"qualification_scope": ["profile-version", "effective-config", "hardware", "source", "transport"],
"preserve_original_source_clock": true,
"report_drops_and_staleness": true,
"overload_is_realtime_pass": false,
"future_onboard_placement_requires_new_measurement": true
},
"packaging": {
"image": "ndc-k1-perception-ddrnet39-rfdetr-tgs:prototype-v1",
"image_sha256": null,
"single_container": true,
"external_model_server_required": false,
"host_model_cache_required": false,
"runtime_model_download_allowed": false,
"model_environment_isolation": "supervised-processes-one-gpu-scheduler",
"common_interpreter_required": false
},
"hardware_baseline": {
"gpu": "NVIDIA GeForce RTX 4090",
"active_profiles_per_worker": 1,
"sources_per_worker": 1,
"gpu_inference_policy": "serialized",
"other_gpu_workloads_allowed": false
},
"cpu_execution_policy": {
"OPENBLAS_NUM_THREADS": 1,
"OMP_NUM_THREADS": 1,
"MKL_NUM_THREADS": 1,
"reason": "bounded-single-thread-numeric-libraries-before-whole-graph-qualification"
},
"operating_envelope": {
"status": "bounded-latency-reference-not-full-qualification",
"checker": "k1link.perception.worker_operating_envelope.operating_envelope_failures",
"reference_conditions": {
"envelope_id": "worker006-4090-610.47-stock-clock-reference/v1",
"gpu_name": "NVIDIA GeForce RTX 4090",
"driver_version": "610.47",
"cpu_limit_millicores": 8000,
"memory_limit_mib": 8192,
"minimum_sm_clock_mhz": 2610,
"minimum_memory_clock_mhz": 10251,
"maximum_snapshot_age_ms": 1000
},
"check_phase": "post-warmup-before-timed-window-plus-runtime-monitoring",
"container_may_set_host_clocks": false,
"persistent_host_clock_policy_installed": false,
"readiness_is_realtime_qualification": false,
"unknown_ownership_or_telemetry_is_pass": false,
"failed_envelope_allows_labelled_experiment": true
},
"output_freshness_contract": {
"schema_version": "missioncore.perception-scene-freshness/v1",
"required_layers": ["segmentation", "objects", "geometry", "motion", "costmap", "policy"],
"retain_original_layer_identity": true,
"propagate_required_input_age": true,
"recheck_at_publication_receipt_and_use": true,
"clock_uncertainty_adds_to_age": true,
"unavailable_or_stale_required_layer": "no-go-advisory-only",
"history_can_independently_authorize_terrain": false,
"transport_streamstart_and_lease_fencing_required_separately": true
},
"input_contract": {
"schema_version": "missioncore.perception-stream-start/v1",
"channels": ["camera", "point-cloud", "pose"],
"camera": {
"width": 800,
"height": 600,
"projection": "KB4",
"decoded_pixel_format": "bgr8",
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
"valid_fov_sha256": "a40cee06b7c6f69b6a09a11563dcfd237f3de833b1ccd31459e66692e528ba63"
},
"point_cloud": {
"representation": "vendor-registered-current-increment",
"coordinate_frame": "map",
"units": "metres",
"scalar_type": "float32-little-endian",
"raw_fields_retained": true
},
"pose": {"parent_frame": "map", "child_frame": "sensor"},
"clock_mapping_required": true,
"source_end_or_duration_required": false,
"full_source_transfer_required": false,
"full_source_hash_pass_required": false,
"full_decode_or_png_cache_required": false,
"previous_perception_artifacts_allowed": false
},
"stream_policy": {
"replay_speed": 1.0,
"release_clock": "original-source-timestamps",
"inference_stride": 1,
"sampling_status": "strict-every-frame-initial-baseline-no-silent-multirate",
"first_result_requires_eof": false,
"lookahead_allowed": false,
"reconnect": "new-epoch-drop-old-backlog-reacquire-codec-keyframe",
"overflow": "bounded-drop-with-ledger-degraded-and-failed-runtime-gate",
"archive_export_on_critical_path": false,
"transport_candidate": "grpc-bidirectional-protobuf-binary-payloads",
"transport_qualified": false
},
"components": {
"segmentation": {
"model": "ddrnet_39-goose-fine-64",
"checkpoint_sha256": "b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6",
"precision": "existing-fp32-baseline",
"preprocessing": "center-600-square-to-512-rgb-tensor-0-1",
"outside_crop": "undefined-not-free",
"class_count": 64,
"runtime": "python-3.9-pytorch-1.13.1-cu117-super-gradients-3.2.0"
},
"objects": {
"model": "rf_detr_large_native_kb4",
"checkpoint_sha256": "0f4e20e19a99c0f8a62b5685f57f6c8b5c371c59081feda6752a0561a79ccf38",
"engine_sha256": "b8a40b3580edff001ec9680de68707242294ff590ab296000fae371f1083f695",
"runtime": "tensorrt-11-fp16-native-uint8",
"minimum_score": 0.25,
"minimum_box_area_pixels": 64,
"maximum_box_area_fraction": 0.5,
"minimum_valid_fov_fraction": 0.5,
"require_center_inside_valid_fov": true,
"required_existing_classes": ["person", "cat", "dog"],
"fine_static_object_classifier_required": false,
"filter_quality_status": "existing-unchanged-near-large-and-small-object-risk-unqualified"
},
"geometry": {
"online_surface": "K1LocalSurfaceShadowEstimator",
"prepared_local_surface_allowed": false,
"generic_static_class": "static.unknown",
"detected_object_range": "median-camera-z-of-owned-current-points/v1",
"geometry_only_range": "nearest-euclidean-sensor-distance/v1",
"vehicle_clearance_claim": false,
"missing_support": "unavailable-not-zero-not-free"
},
"motion": {
"reuse": "BoundedSpatialTemporalProvider+ClassIndependentMotionEstimator",
"semantic_class_implies_motion": false
},
"costmap": {
"algorithm": "TRAVEL-TGS",
"revision": "95dc2fbd66a343efd9060c45a5711b6307a950a4",
"execution": "cpu",
"rolling_history_seconds": 1.0,
"states": ["UNOBSERVED", "GROUND_SUPPORT", "NONGROUND_OCCUPIED", "UNKNOWN_REJECTED"],
"policy_input": true
},
"policy": {
"scope": "rural-prototype-advisory-only",
"hard_surface": "ALLOW-candidate-subject-to-geometry-and-freshness",
"fine_road_sidewalk_bikeway_distinction_required": false,
"effective_material_rules_required": true,
"unknown_or_occupied_overrides_material_allowance": true,
"sensor_to_vehicle_mount": "simulation-only-until-measured"
}
},
"reference_files": [
{"path": "config/perception/rf-detr-large-native-kb4-risk-shadow-v0.json", "sha256": "dbf4da5dbad6c3c22b1280b46ffcad81719bd183c81c263a4859847d829019b6"},
{"path": "config/perception/lab-v1-goose-vegetation-benchmark-v1.json", "sha256": "96a427a8baae387b827ec9c0bf7ca42e3fb9114b8fa9a8671bbc9d10877670b9"},
{"path": "config/perception/lab-v1-vegetation-provider-label-map-v1.json", "sha256": "f2b69046b6a740fd9532d2d88e7fabae7c20fb662f783c9502adc9026406f352"},
{"path": "config/perception/lab-v1-vegetation-mission-policy-v1.json", "sha256": "b75c4ac841d7b4bcc57f7a9c8417ca2317d8ecfa499e72a9af8a8591a2ec0d35"},
{"path": "config/perception/m49-tgs-full-shadow-v1.json", "sha256": "c2e07010aaee78259d36c057962d6bfb885349251ff7356d867e5813e632881c"},
{"path": "config/perception/m4-geometry-association-v1.json", "sha256": "cc666c9389a5e221957faddec89584709b66918d14abaf646f1832e001421999"}
],
"reference_usage": "reuse-model-parameters-and-algorithms-only-not-recording-ids-or-precomputed-results",
"realtime_contract": {
"schema_version": "missioncore.perception-realtime-contract/v1",
"budget_status": "engineering-targets-worker-pilot-measured-not-whole-path-qualified-not-safety-approved",
"budgets": {
"maximum_start_metadata_bytes": 65536,
"maximum_chunk_bytes": 1048576,
"maximum_inflight_bytes": 16777216,
"maximum_pending_camera_frames": 2,
"maximum_codec_preroll_ms": 500.0,
"maximum_pose_age_ms": 100.0,
"maximum_clock_uncertainty_ms": 5.0,
"maximum_release_lag_ms": 25.0,
"maximum_output_age_p95_ms": 125.0,
"maximum_output_age_p99_ms": 125.0,
"maximum_layer_age_ms": 250.0,
"maximum_first_result_ms": 1000.0,
"maximum_warmup_seconds": 120.0,
"maximum_stop_seconds": 5.0,
"maximum_vram_mib": 22000,
"maximum_rss_mib": 8192,
"maximum_backlog_growth_ms": 25.0
},
"budget_origin": "125ms-historical-envelope-retained-as-target-now-applied-to-whole-path; other-limits-explicit-initial-engineering-guards",
"capacity_drop_count_max": 0,
"unaccounted_input_count_max": 0,
"full_input_transfer_before_first_result_allowed": false,
"independent_quality_and_physical_live_status": "pending"
},
"authority": {
"commands_enabled": false,
"actuation_allowed": false,
"navigation_or_safety_accepted": false,
"production_accepted": false
}
}
@@ -0,0 +1,162 @@
{
"schema_version": "missioncore.lab-v1-eomt-ddrnet-portable-profile/v2",
"profile_id": "lab-v1-eomt-ddrnet-portable-v2",
"lab_id": "LAB-V1",
"worker_id": "worker-006",
"source_binding": {
"mode": "admitted-k1-recording",
"camera_source_id": "sensor.camera.right",
"camera_timeline": "dynamic",
"filesystem_paths": "executor-resolved",
"expected_width": 800,
"expected_height": 600,
"expected_frame_count": "source-derived",
"frame_indices": "source-derived-representative",
"crop_contract": "center-600-square-to-512; outside-crop-is-undefined"
},
"effective_config_contract": {
"schema_version": "missioncore.lab-v1-goose-vegetation-benchmark/v1",
"dynamic_field": "ravnoves",
"source_id": "sealed-source-derived",
"source_sha256": "sealed-camera-input-derived",
"base_m4_result_id": null
},
"runtime": {
"super_gradients_version": "3.2.0",
"super_gradients_revision": "54d062ecb1081944a672ce447cf3e96a36708ff9",
"python_version": "3.9",
"numpy_version": "1.23.0",
"cmake_version": "3.31.6",
"onnxsim_version": "0.4.36",
"opencv_python_version": "4.8.1.78",
"pytorch_version": "1.13.1",
"torchvision_version": "0.14.1",
"pytorch_cuda_version": "11.7",
"container_base": "nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04@sha256:ad6d59a3bbf3e82c1c849c9ac09cfc2a3e0bbb8655042fd899be6681b3fe2a85",
"miniconda_installer": "Miniconda3-py39_24.11.1-0-Linux-x86_64.sh",
"miniconda_installer_sha256": "3ea8373098d72140e08aac9217822b047ec094eb457e7f73945af7c6f68bf6f5"
},
"dataset": {
"dataset_id": "goose-2d-validation-visible-rgb",
"relative_root": "goose-2d/validation",
"mapping_relative_path": "goose_label_mapping.csv",
"mapping_sha256": "88ae319ba5a3877dd3ae0773f693a6a5fdc283934140de9dfaff029108aefd7f",
"image_glob": "images/val/**/*_windshield_vis.png",
"expected_pair_count": 962,
"input_size": [
512,
512
],
"preprocessing": [
"center-square-crop",
"nearest-neighbor-resize",
"rgb-to-tensor-0-1"
]
},
"candidates": {
"ddrnet": {
"candidate_id": "goose-ddrnet-class-512",
"model_names": [
"ddrnet_39"
],
"checkpoint_relative_path": "models/goose/ddrnet_class_512.pth",
"checkpoint_size_bytes": 259419077,
"checkpoint_sha256": "b99c2838051bcd7b092fd3970aa62a77d5c0bbb809c9b9afb2ff4b0ebdaa4ee6",
"published_validation_miou_percent": 46.53
}
},
"vegetation_class_names": [
"leaves",
"forest",
"bush",
"moss",
"tree_crown",
"tree_trunk",
"crops",
"low_grass",
"high_grass",
"scenery_vegetation",
"hedge",
"tree_root"
],
"visual_case_contract": {
"selection_basis": "ground-truth-class-support-only",
"case_count": 12,
"minimum_focus_pixels": 2048,
"strata": [
{
"class_name": "high_grass",
"count": 2
},
{
"class_name": "low_grass",
"count": 2
},
{
"class_name": "bush",
"count": 2
},
{
"class_name": "tree_trunk",
"count": 2
},
{
"class_name": "tree_crown",
"count": 1
},
{
"class_name": "hedge",
"count": 1
},
{
"class_name": "forest",
"count": 1
},
{
"class_name": "crops",
"count": 1
}
],
"error_overlay": {
"correct_material_rgba": [
34,
197,
94,
72
],
"missed_vegetation_rgba": [
239,
68,
68,
220
],
"false_vegetation_rgba": [
245,
158,
11,
220
],
"wrong_vegetation_material_rgba": [
168,
85,
247,
220
]
}
},
"policy_action_colors": {
"ALLOW": "#22c55e",
"HIGH_COST": "#f59e0b",
"NO_GO": "#ef4444"
},
"invariants": {
"one_heavy_candidate_at_a_time": true,
"raw_fisheye_is_immutable": true,
"outside_center_crop_is_free": false,
"missing_or_unknown_is_free": false,
"camera_semantics_can_clear_rigid_geometry": false,
"navigation_authority": false,
"actuation_authority": false,
"canonical_triton_mutation_allowed": false
}
}
@@ -0,0 +1,60 @@
{
"schema_version": "missioncore.m49-tgs-portable-profile/v2",
"profile_id": "m49-tgs-portable-v2",
"source_binding": {
"mode": "admitted-k1-recording",
"camera_timeline": "dynamic",
"lidar_replay": "dynamic",
"trajectory": "dynamic",
"frame_counts": "source-derived",
"filesystem_paths": "executor-resolved"
},
"alignment": {
"timeline": "recorded-camera-host-arrival",
"lidar_selection": "latest-not-newer-than-camera-frame",
"maximum_lidar_age_seconds": 1.0,
"future_frames_allowed": false
},
"tgs": {
"max_range_m": 80.0,
"min_range_m": 1.0,
"resolution_m": 8.0,
"num_iterations": 3,
"num_lowest_representative_points": 5,
"minimum_points": 10,
"seed_threshold_m": 0.5,
"distance_threshold_m": 0.125,
"outlier_threshold_m": 0.3,
"normal_threshold": 0.94,
"weight_threshold": 200.0,
"lcc_normal_similarity": 0.03,
"lcc_planar_distance_m": 0.1,
"obstacle_height_m": 1.0,
"refine_mode": true
},
"rolling_profile": {
"history_seconds": 1.0,
"local_radius_m": 12.0,
"missing_lidar_policy": "all-cells-unobserved"
},
"costmap": {
"coordinate_frame": "map-gravity-local",
"cell_size_m": 0.45,
"radius_m": 12.0,
"state_priority": [
"NONGROUND_OCCUPIED",
"UNKNOWN_REJECTED",
"GROUND_SUPPORT",
"UNOBSERVED"
]
},
"invariants": {
"aos_allowed": false,
"gpu_allowed": false,
"missing_support_means_free": false,
"missing_lidar_means_unobserved": true,
"unobserved_cells_are_emitted": true,
"camera_projection_is_authoritative": false,
"navigation_or_actuation_allowed": false
}
}
+33 -2
View File
@@ -20,10 +20,23 @@ The normalizer exposes a read-only normalized telemetry adapter on
Timescale credentials, and Timescale is not published on a host port.
The normalizer uses a separate `missioncore_normalizer` database role with only
`SELECT`, `INSERT`, `UPDATE`, and bounded retention `DELETE` on the telemetry
hypertable. Raw telemetry older than 30 days is removed at most once per day by the
normalizer. This stays compatible with the Apache-licensed Timescale image without
hypertable. Raw telemetry older than 30 days is removed in batches of at most
1000 rows per minute, on a separate connection/thread (2s statement / 250ms lock
deadline). Retention never runs inside the MQTT callback. This stays compatible
with the Apache-licensed Timescale image without
depending on the Timescale License retention scheduler.
The 1 GiB database container explicitly sets 128 MiB shared buffers and 64 MiB
maintenance memory. The HA image's automatic tuning sees the Docker VM instead
of this container limit; do not inherit its multi-GiB defaults.
For a memory-constrained local repair, `compose.source.yaml` mounts the reviewed
normalizer source read-only into the **existing** image. Set
`MISSIONCORE_NORMALIZER_SOURCE` to the absolute `normalizer.py` path, pass both
Compose files and the existing private `--env-file` / `--project-directory`, and
use `up -d --no-deps --no-build normalizer`. This is an explicit local source
override, not a rebuilt portable image. Normal restart/reboot retains the mount.
The stack is one deployment contour, not one multi-process container. Keeping broker,
normalizer and database in separate containers preserves independent health checks,
least-privilege boundaries and rollback while Compose provides one operator lifecycle:
@@ -136,6 +149,24 @@ globally unique because Mosquitto ACL ownership is username-based.
## Worker agent
MQTT outputs use `startup_error_behavior="retry"`, with a 2000-metric buffer and
the configured flush interval. A missing broker at agent startup must not end
the service. See [Telegraf's startup policy](https://docs.influxdata.com/telegraf/v1/configuration/plugin-options/).
If Docker lost a published listener while the saved LAN address is unchanged,
the explicit broker Apply action reconciles that listener; a telemetry GET never
restarts infrastructure. Node connectivity does not prove a profile is ready.
The streaming profile can export one bounded (8 KiB) current observation using
`--telemetry-snapshot /telemetry/current.json`, with `/telemetry` mounted to
`C:\ProgramData\NDC\MissionCore\telemetry-agent\perception` on Worker. The existing
collector reads it with a 5s freshness limit and forwards it over the same MQTT
pipeline topic. It overrides the legacy service only when explicitly present;
an expired/broken file reports unavailable rather than relabeling a legacy model.
The publisher runs independently from heartbeat/inference and never renews a
lease, grants GPU ownership, or qualifies real-time. Queue memory includes active
input and decoder scratch. Stage timings are unavailable until actually measured.
No camera, point cloud, model result, MQTT secret, or motor command goes here.
Worker 006 uses the official Windows Telegraf distribution as the host service
`NDC Mission Core Telemetry Agent`. Install or update it with:
@@ -0,0 +1,9 @@
# Local, read-only source override for repairing the existing normalizer without
# rebuilding an image on a memory-constrained operator machine. No extra service.
services:
normalizer:
volumes:
- type: bind
source: ${MISSIONCORE_NORMALIZER_SOURCE:?Set the absolute normalizer.py path}
target: /app/normalizer.py
read_only: true
+2
View File
@@ -49,6 +49,8 @@ services:
timescale:
image: timescale/timescaledb-ha:pg16.14-ts2.28.2-all-oss
# The HA image auto-tunes for the Docker VM, not this 1 GiB container.
command: ["postgres", "-c", "shared_buffers=128MB", "-c", "work_mem=4MB", "-c", "maintenance_work_mem=64MB", "-c", "effective_cache_size=512MB", "-c", "max_parallel_workers_per_gather=0", "-c", "max_connections=32"]
container_name: ndc-mission-core-telemetry-timescaledb
restart: unless-stopped
security_opt:
+39 -16
View File
@@ -31,7 +31,8 @@ SAFE_IDENTIFIER: Final = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$")
MAX_PAYLOAD_BYTES: Final = 1024 * 1024
MAX_TAGS: Final = 256
MAX_SERIES_KEY_BYTES: Final = 4096
RETENTION_INTERVAL_SECONDS: Final = 86_400
RETENTION_INTERVAL_SECONDS: Final = 60
RETENTION_BATCH_ROWS: Final = 1000
ALLOWED_TELEMETRY_TAGS: Final = frozenset(
{
"agent_id",
@@ -434,6 +435,39 @@ def _start_query_server(
return server
def _retention_batch(connection: Any) -> int:
"""Preserve the existing 30-day policy without an unbounded startup DELETE."""
with connection.transaction(), connection.cursor() as cursor:
cursor.execute("SET LOCAL statement_timeout = '2s'")
cursor.execute("SET LOCAL lock_timeout = '250ms'")
cursor.execute(
"""
WITH expired AS (
SELECT tableoid, ctid FROM contour_telemetry_samples
WHERE observed_at < CURRENT_TIMESTAMP - INTERVAL '30 days'
ORDER BY observed_at LIMIT %s
)
DELETE FROM contour_telemetry_samples AS samples USING expired
WHERE samples.tableoid = expired.tableoid AND samples.ctid = expired.ctid
""",
(RETENTION_BATCH_ROWS,),
)
return cursor.rowcount
def _retention_loop(dsn: str) -> None:
import psycopg # type: ignore[import-not-found]
while True:
# Neither a retention timeout nor database recovery blocks MQTT I/O.
time.sleep(RETENTION_INTERVAL_SECONDS)
try:
with psycopg.connect(dsn, connect_timeout=3) as connection:
_retention_batch(connection)
except psycopg.Error:
print("telemetry retention deferred", flush=True)
def main() -> None:
import paho.mqtt.client as mqtt
import psycopg # type: ignore[import-not-found]
@@ -447,7 +481,9 @@ def main() -> None:
password = _required("MISSIONCORE_MQTT_PASSWORD")
runtime_health = TelemetryRuntimeHealth()
connection = psycopg.connect(dsn, autocommit=True)
last_retention_monotonic = 0.0
threading.Thread(
target=_retention_loop, args=(dsn,), name="telemetry-retention", daemon=True
).start()
runtime_health.set_database(True)
_start_query_server(dsn, runtime_health)
client = mqtt.Client(
@@ -480,7 +516,7 @@ def main() -> None:
runtime_health.set_mqtt(False, f"MQTT disconnected: {reason_code}")
def on_message(_client: Any, _userdata: object, message: Any) -> None:
nonlocal connection, last_retention_monotonic
nonlocal connection
try:
row = _normalize(message.topic, message.payload)
except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
@@ -489,19 +525,6 @@ def main() -> None:
for attempt in range(2):
try:
with connection.cursor() as cursor:
now_monotonic = time.monotonic()
if (
now_monotonic - last_retention_monotonic
>= RETENTION_INTERVAL_SECONDS
):
cursor.execute(
"""
DELETE FROM contour_telemetry_samples
WHERE observed_at
< CURRENT_TIMESTAMP - INTERVAL '30 days'
"""
)
last_retention_monotonic = now_monotonic
cursor.execute(
"""
INSERT INTO contour_telemetry_samples (
@@ -1,7 +1,8 @@
[CmdletBinding()]
param(
[string]$ContainerName = "ndc-mission-core-perception-worker",
[string]$HealthUrl = "http://127.0.0.1:18020/health"
[string]$HealthUrl = "http://127.0.0.1:18020/health",
[string]$RuntimeSnapshot = "C:\ProgramData\NDC\MissionCore\telemetry-agent\perception\current.json"
)
$ErrorActionPreference = "Stop"
@@ -18,6 +19,46 @@ $stageIds = @(
)
$health = $null
$collectorState = "unavailable"
# A fresh full-profile export supersedes the historical service. Presence of an
# expired/broken export is NOT permission to relabel a legacy model as current.
if (Test-Path -LiteralPath $RuntimeSnapshot -PathType Leaf) {
$runtime = $null
try {
$item = Get-Item -LiteralPath $RuntimeSnapshot
$age = ([DateTime]::UtcNow - $item.LastWriteTimeUtc).TotalSeconds
if ($item.Length -le 8192 -and $age -ge -1 -and $age -le 5) {
$stream = [IO.File]::Open($RuntimeSnapshot, 'Open', 'Read', 'ReadWrite')
try {
$bytes = New-Object byte[] 8193
$count = $stream.Read($bytes, 0, $bytes.Length)
if ($count -le 8192) {
$runtime = [Text.Encoding]::UTF8.GetString($bytes, 0, $count) | ConvertFrom-Json
}
} finally { $stream.Dispose() }
if ($runtime.schema_version -ne 'missioncore.perception-runtime-observation/v1') {
$runtime = $null
}
}
} catch { $runtime = $null }
$samples = foreach ($stageId in $stageIds) {
$sample = [ordered]@{
stage_id = $stageId
stage_state = 'unavailable'
service_state = 'unavailable'
collector_state = 'unavailable'
}
if ($runtime) {
$sample.collector_state = 'live'
foreach ($name in @('service_state', 'profile_name', 'active_request_id',
'input_state', 'wait_reason', 'live_children', 'input_pauses', 'pending_bundles', 'buffer_bytes')) {
$sample[$name] = $runtime.$name
}
}
[pscustomobject]$sample
}
@($samples) | ConvertTo-Json -Compress -Depth 4
exit 0
}
try {
$healthJson = docker exec $ContainerName python3 -c `
"import urllib.request;print(urllib.request.urlopen('$HealthUrl',timeout=2).read().decode())" `
@@ -68,13 +109,13 @@ $completedRuns = if ($health -and $null -ne $health.completed_runs) {
[int64]$health.completed_runs
}
else {
[int64]0
[int64]-1
}
$failedRuns = if ($health -and $null -ne $health.failed_runs) {
[int64]$health.failed_runs
}
else {
[int64]0
[int64]-1
}
$modelLoadSeconds = if ($health -and $null -ne $health.model_load_seconds) {
[double]$health.model_load_seconds
@@ -1,5 +1,8 @@
[agent]
interval = "${MISSIONCORE_TELEMETRY_INTERVAL}"
flush_interval = "${MISSIONCORE_TELEMETRY_INTERVAL}"
metric_batch_size = 200
metric_buffer_limit = 2000
round_interval = true
omit_hostname = false
@@ -26,6 +29,7 @@
total = true
[[outputs.mqtt]]
startup_error_behavior = "retry"
servers = ["tcp://${MISSIONCORE_MQTT_HOST}:${MISSIONCORE_MQTT_PORT}"]
topic = "mission-core/v1/contours/${MISSIONCORE_CONTOUR_ID}/agents/${MISSIONCORE_AGENT_ID}/host"
username = "${MISSIONCORE_MQTT_USERNAME}"
@@ -1,5 +1,8 @@
[agent]
interval = "${MISSIONCORE_TELEMETRY_INTERVAL}"
flush_interval = "${MISSIONCORE_TELEMETRY_INTERVAL}"
metric_batch_size = 200
metric_buffer_limit = 2000
round_interval = true
omit_hostname = false
@@ -30,6 +33,7 @@
[[inputs.docker]]
endpoint = "npipe:////./pipe/docker_engine"
container_name_include = []
container_state_include = ["running", "exited", "restarting", "paused"]
[[inputs.http_response]]
urls = ["http://127.0.0.1:8000/v2/health/ready"]
@@ -59,6 +63,9 @@
"current_stage",
"active_request_id",
"collector_state",
"profile_name",
"input_state",
"wait_reason",
]
[[inputs.tail]]
@@ -73,6 +80,7 @@
data_type = "string"
[[outputs.mqtt]]
startup_error_behavior = "retry"
servers = ["tcp://${MISSIONCORE_MQTT_HOST}:${MISSIONCORE_MQTT_PORT}"]
topic = "mission-core/v1/contours/${MISSIONCORE_CONTOUR_ID}/agents/${MISSIONCORE_AGENT_ID}/host"
username = "${MISSIONCORE_MQTT_USERNAME}"
@@ -83,6 +91,7 @@
namedrop = ["missioncore_pipeline", "missioncore_pipeline_event"]
[[outputs.mqtt]]
startup_error_behavior = "retry"
servers = ["tcp://${MISSIONCORE_MQTT_HOST}:${MISSIONCORE_MQTT_PORT}"]
topic = "mission-core/v1/contours/${MISSIONCORE_CONTOUR_ID}/agents/${MISSIONCORE_AGENT_ID}/pipeline"
username = "${MISSIONCORE_MQTT_USERNAME}"
+90
View File
@@ -275,6 +275,96 @@ execution canaries. Once they prove one automatic run-to-evidence path, the LAB
canonicalization slice closes and remaining historical work stays `legacy` unless
an integrity or product need justifies a targeted migration.
## Observatory durable recorded queue and Worker dispatch
Selecting a source and a laboratory setup in Observatory is not itself a run.
The exact legacy submission contains only `source_session_id`, `setup_id` and an
idempotency key. A portable submission additionally returns the server-owned
`definition_sha256` and one content-bound `check_sha256` obtained from preflight;
both must be echoed unchanged during submit. Neither request can supply commands,
executable text, filesystem paths, container images, model identities, resource
limits or priority. The server resolves the allowlisted identities and seals all
executable identity into one durable record:
- the current source-catalog snapshot captured at admission, plus immutable source
bundle and source-capability-manifest SHA-256 identities;
- source-adapter id, version and SHA-256;
- setup RunDefinition id, version and SHA-256;
- executor release and image SHA-256 identities;
- learned-model manifest and resource-profile identities;
- checkpoint policy and the server-owned priority class.
An exact retry returns the existing job. Reusing an idempotency key for another
identity fails closed. The ordinary lifecycle advances through `accepted`,
`queued`, `claimed` and `running`, then terminates as `succeeded` or `failed`.
`paused`, `preemption-pending` and `reconciliation-required` are explicit safety
branches rather than hidden retries. The durable queue serializes one recorded
compute owner and does not equate a queued receipt with a Worker execution
receipt.
The only pair currently admitted to the durable queue is the exact `RAVNOVES00`
source and `M4.9T5 · TRAVEL TGS · CPU-only, без ML` RunDefinition. It seals an
explicitly empty learned-model list because TGS is an algorithmic CPU pipeline,
not an unknown model dependency. The binding pins the exact source pack rather
than a volatile whole-catalog digest; the server captures and seals the current
catalog snapshot into each admitted job.
Two source-independent definitions are projected by the portable catalog:
- `LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39` seals the exact
model/component/resource identities and the
`missioncore.recorded-eomt-ddrnet-review/v2` result contract;
- `M4.9T5 · TRAVEL TGS · CPU-only, без ML` v2 seals an explicitly empty model
manifest, dynamic source-derived frame counts, causal TGS invariants and the
`missioncore.recorded-tgs-costmap-review/v2` result contract.
Neither portable definition contains a source Session id, label, fixed frame
count or filesystem path. Compatibility is derived independently for every
setup from the selected Session's real K1 capabilities. A compatible source may
therefore report capability `pass` while that setup's executor remains blocked.
Blocked definitions stay projectable and do not prevent an unrelated ready
definition from entering the durable queue allowlist.
The server now owns a definition-SHA/check-SHA fenced portable check/submit
boundary. Submission still fails closed unless that exact definition has a
sealed `ready` executor release and image and is present in the durable queue.
Both repository portable definitions currently declare `not-installed`, so
their preflight remains blocked; no release or image hash is fabricated. The
generic v2 assemblers, verified publisher and Worker transport exist as a
dormant fail-closed implementation, but exact executor installation, central
storage, authentication, tunnel acceptance and end-to-end canaries remain
pending. The old `missioncore.lab-v1-vegetation-shadow/v1` result and the exact
RAVNOVES00 M4.9 result remain only in their existing immutable catalogs;
neither is reclassified as a result of a portable definition.
Recorded work has priority rank `100`. A future live K1 lease has rank `0` and
closes new recorded claims while it is pending or active. Cooperative executors
yield only at an allowlisted checkpoint. A non-checkpointable monolith must be
cancelled by the scheduler, discard all staging output and later restart from
zero. Cancellation uses a crash-safe two-phase protocol: Mission Core first
persists one stable cancellation intent and activates live work only after an
identity-bound receipt proves resource release and staging discard. A retry uses
the same cancellation identity. A missing callback or receipt remains durably
pending for retry; a conflicting or indeterminate receipt enters reconciliation.
Every unresolved form blocks live activation and concurrent ownership.
An explicit live terminal trigger requeues paused recorded work.
ADR 0046 records this decision. Durable queue and identity contracts, the
authenticated Worker pull protocol and transport-agnostic Worker agent exist as
foundation. The production app hard-disables the Worker router even when a valid
credential is present until an expiring claim lease and verified result publisher
are accepted. Installation of the exact executors, Worker deployment and wiring
from the real K1 lifecycle to live-lease triggers remain pending. Therefore a
submitted exact legacy M4.9T5 job may honestly wait in `queued` without implying
that Worker 006 can execute it yet. Portable LAB V1 and portable M4.9T5 cannot
currently be submitted because their executor states are `not-installed` and
the authenticated Worker dispatch gate is closed.
Worker telemetry remains secondary observation evidence. It does not replace the
authoritative queue ledger, result validation or common laboratory receipt. K1
acquisition and control, the legacy LAB archive, and Simulation/Gaussian runtimes
are outside this boundary and are not restarted, migrated or modified by it.
## Planning discipline
A planned LAB number is not a placeholder page. Before execution it must name:
+11 -5
View File
@@ -227,9 +227,13 @@ Mission Core already has three semantic mechanisms:
3. `docs/domain-model/mission-core-experimental-vocabulary-v0alpha2.md` for
meanings that are not yet stable enough for Platform Ontology.
These mechanisms are sufficient before A3. A new RDF/graph store, ontology
service, or parallel entity catalog would introduce duplicated identity and
migration work without a current query or integration consumer.
These mechanisms were sufficient before A3. Observatory modular compositions
crossed the gate on 2026-09-04: planning, Worker dispatch, LAB publication and
replay presentation require the same module/composition/result/layer relations,
and typed single-result traversal lost the TGS member of a composed run. The
bounded local package is defined by
`docs/domain-model/observatory-domain-ontology-v1.md`. It keeps identity in the
existing stores and has no Platform Ontology runtime dependency.
A formal local runtime ontology is introduced only when:
@@ -239,8 +243,10 @@ A formal local runtime ontology is introduced only when:
- the graph answers named queries used by the product or automation;
- promotion or synchronization with NODE.DC Platform Ontology is defined.
Until those conditions hold, new stable meanings extend the versioned local
vocabulary and executable contracts. They do not create a second runtime model.
Other domains continue to extend the versioned local vocabulary and executable
contracts until they independently satisfy these conditions. The admitted
Observatory package projects existing store identities and cannot become a
second authority for them.
## Automated boundary gate
+177
View File
@@ -0,0 +1,177 @@
# Observatory — product-surface brief
Status: accepted by the product owner for the first M5.1 vertical slice on 2026-08-30.
## Operator and job story
The operator is an engineer qualifying the perception stack before any control authority is put on
an unmanned platform. After a source session has been recorded, the engineer needs one lightweight
place to identify that immutable source and see which laboratory results are actually linked to it.
The first slice is used repeatedly while recordings and results are being produced; it is not a
mission-planning surface and it never sends navigation, braking or actuation commands.
## Placement decision
The accepted placement is `Тестировочный контур → Обсерватория`, with the operator-facing
description `Сессии и квалификация`. It is a third workspace alongside the existing
`Лабораторные контуры` and `Симуляции` entries.
Alternatives considered:
1. Replace or refactor `Лабораторные контуры` in place. Rejected for M5.1 because the current LAB
archive is a working, documented legacy projector and rollback reference.
2. Add an Observatory mode to `Данные → Сессии и записи`. Rejected because that surface owns
storage and replay, while Observatory owns the recurring qualification-review job.
3. Add a dedicated workspace under `Тестировочный контур`. Selected because it creates a clean
composition boundary without changing K1, Simulation or the legacy LAB lifecycle.
## Entity, evidence and authority
The source entity is an immutable observation Session. Its visible facts are limited to the
validated Session catalog contract: identity, timestamps, status, duration, modalities and
readability. Laboratory evidence is linked only by the typed `lab.sourceSessionId` relationship.
Source and LAB projections are independent bounded newest-first windows. A LAB result whose source
is absent from the loaded source window remains unresolved and visible as `вне среза`; it is never
attached heuristically and its absence is not presented as a broken relationship. Reaching the
100-item boundary marks historical completeness as unknown until cursor pagination exists.
The presence of a Session or linked LAB result is not a computer-vision pass and is not evidence of
safe navigation. M5.1 does not infer a verdict from provenance, labels or result kind. The workspace
is observation-only: command, navigation and safety authority are all absent.
## Viewer-profile boundary
The existing source-specific viewer contracts remain separate and unchanged:
| Source job | Existing profile | Clock / load ownership |
| --- | --- | --- |
| Live equipment | `live-acquisition` | `stream_time`; live receiver and recovery authority |
| Historical Sessions | `recorded-session` | `session_time`; explicit preparation and progressive admission |
| Legacy LAB evidence | `lab-recorded-evidence` | `source-sequence`; explicit comparison-only loading |
Canonical LAB compositions may configure the recorded Rerun engine inside the bounded LAB slice,
but that does not make Observatory a LAB or live consumer. The first Observatory slice mounts no
viewer while its catalog is opened, refreshed or navigated. Its bounded canary may activate only
the historical `recorded-session` profile after a separate explicit action and acceptance proof.
It does not add a fourth profile or silently collapse the three existing lifecycles.
## M5.1 canary: canonical recorded replay
The accepted viewer canary reuses the stabilized full-route RAV004 composition; it does not create
a second viewer or a second recorded-data pipeline. The sealed vegetation result is first projected
into the generic Session/LAB catalog through `SessionStore.publish_lab_instance`. Its exact
`route_full_review.session_id` is the only source relationship. Labels, `latest`, result prefixes and
the advanced LAB index are not valid joins.
The immutable LAB projection carries a versioned replay capability:
```json
{
"schema_version": "missioncore.observation-lab-replay-capability/v1",
"kind": "canonical-recorded-rerun",
"viewer_profile": "recorded-session",
"timeline": "session_time",
"activation": "explicit",
"commands_enabled": false
}
```
Catalog entry, Session selection and result selection remain lightweight. Only the explicit
`Открыть визуальный разбор` action reads and revalidates the sealed result metadata. Only after that
admission does the application mount the shared canonical replay component, which follows the
existing `resolveObservationSessionReplay → resolveCanonicalLabReplay → recordedSessionRerunProfile
→ RerunViewport` path. Closing the review or changing Session unmounts that component and aborts
pending admission. At most one viewer and one `session_time` clock exist.
The publication command verifies the registered result root, schema, content-addressed identity,
artifact hashes, exact RAV004 source Session and all false authority flags before adding an
idempotent SQLite projection. The validated source snapshot is compared again inside the same
SQLite write transaction, so reconciliation cannot swap the Session between admission and copy.
The default v1 catalog continues to hide capability-owned projections from legacy consumers; the
explicit v2 LAB catalog returns the typed capability. A narrowly matched rolling migration types
the already-published canonical projection and leaves ordinary legacy LAB rows untouched. The
command does not run inference, ffmpeg, RRD merge or replay
materialization; source/result evidence is not copied or rewritten. Legacy LAB continues to import
the same shared replay through a compatibility wrapper.
## Information hierarchy and states
1. Catalog authority and refresh state.
2. Explicit source Session selection.
3. Selected Session identity, timing, modalities and readability.
4. Strictly linked immutable LAB evidence.
5. Fail-visible bounded-window and unresolved-evidence status.
The admitted states are initial loading, ready with items, ready empty, refreshing with the last
valid snapshot, and unavailable/error with retry. If a selected Session disappears after refresh,
selection moves to the first valid source or to the empty state. No demo rows, fabricated progress,
placeholder actions or hidden polling are allowed.
## Session-to-setup configurator slice
The next bounded slice adds an explicit laboratory Setup selection beside the source Session. A
Setup is a versioned, immutable compatibility contract, not a mutable bag of UI parameters. The
catalog keeps two independently named historical facts:
- `M4.9T5 · TRAVEL TGS · CPU-only, без ML` is an archived RunDefinition bound exactly to
`20260720T065719Z_viewer_live`. Its configuration references are content-addressed, its primary
visual result remains in legacy LAB, and the integrated-graph result is retained as a compute
successor rather than presented as a second viewer.
- `LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39` is the current exact RAV004 result bound to
`20260828T130511Z_viewer_live`. It predates the product RunDefinition contract and therefore keeps
`run_definition = null`; the UI identifies it as an existing result and never fabricates a config
digest for it.
Changing Session fetches only the small setup catalog. Changing Setup performs no computation.
Compatibility requires the exact source Session identity, the preserved source label and all
required modalities. An explicit `Проверить совместимость` action sends the selected source, setup
and RunDefinition digest to a read-only preflight. Digest drift fails closed. The preflight may
report an exact existing result or a blocked executor, but it cannot enqueue work.
The calculation action remains absent until a durable dispatcher exists with an idempotent
submission key, immutable RunDefinition receipt, lifecycle ledger and exact result publication
receipt. The currently preserved bespoke Worker scenario is not silently treated as that adapter.
This boundary also keeps live equipment, historical replay and legacy LAB Rerun profiles separate.
## Design Guideline composition
The existing `ApplicationShell`, `AdminNavigationPanel` and `ApplicationPanel` composition remains
the owner of navigation and workspace framing. The feature reuses canonical `Select`, `Button`,
`Icon`, `StatusBadge`, `GlassSurface` and `ActivityIndicator` primitives. The semantic Session and
evidence projection belongs to `workspaces/observatory`; feature styles own layout only and do not
introduce a parallel control or surface grammar. The registered `eye` icon identifies the new
workspace.
## First-slice acceptance
- Polygon navigation order is `Лабораторные контуры → Симуляции → Обсерватория`.
- Opening Observatory loads only the typed source and laboratory Session catalogs.
- Opening Observatory mounts no Rerun, canvas, WebGL, RRD, WebSocket or LAB result route.
- Source and LAB records are joined only by `sourceSessionId`; evidence outside the independently
bounded source window remains visible as unresolved and never becomes a false integrity verdict.
- Reaching either 100-item catalog boundary explicitly marks historical completeness as unknown.
- The selected source presents at most six newest linked results while retaining the exact total;
the historical tail remains in legacy LAB and is never mounted into a long Observatory DOM.
- Loading, empty, refreshing, error/retry and ready states contain no synthetic data.
- Linked evidence is never presented as a CV or safety pass.
- Existing LAB, Simulation, K1 and Data/Sessions operator behavior remains unchanged.
- Live, historical Session and legacy LAB viewer-profile contracts remain distinct.
- Session and Setup are independently explicit selections; neither selection submits Worker work.
- Both the archived M4.9T5 setup and the current final RAV004 result remain visible and immutable.
- A pre-RunDefinition result never receives a fabricated configuration hash.
- Preflight is read-only, digest-fenced and reports the missing executor adapter instead of
fabricating queued or running states.
## Canary acceptance
- RAV004 appears as one exact LAB result linked to `20260828T130511Z_viewer_live` in the Session
catalog; Observatory never reads `/api/v1/laboratory/advanced-index`.
- Entering Observatory and changing Session perform no replay POST, RRD HEAD/GET, viewer mount or
canvas creation.
- Explicit open validates the result/source binding, then uses the existing canonical RRD,
blueprint, cache, `recorded-session` profile and `session_time` clock.
- One `RerunViewport` is mounted; legacy clip/Three renderers, a second clock and a second RRD
builder are absent.
- Close, Session change and run replacement abort pending work and fully unmount the viewer.
- K1, Worker 006/add-worker, Simulation/Gaussian, raw Sessions and legacy LAB behavior remain
unchanged.
File diff suppressed because it is too large Load Diff
@@ -110,3 +110,48 @@ not a realtime inference or navigation claim.
- The migration does not improve DDRNet quality, prove terrain traversability
or grant navigation/actuation authority. Those remain separate model and
safety acceptance questions.
## Resource ownership addendum — 2026-09-03
An active calculation/view may allocate what its admitted workload needs. On
termination its ephemeral resources must become releasable; lowering evidence
quality or deleting durable results is not a substitute for lifecycle cleanup.
Recorded viewers run the same unmodified upstream SDK in a disposable,
same-origin iframe inside the existing viewer surface. A small synchronous
facade exposes only the public methods already used by the product adapter.
The revision-2 boundary returns primitive JSON strings, including events and
errors; the parent parses them in its own realm. Native handles, DOM nodes,
channels, unsubscribe closures and SDK startup Promises stay inside the iframe.
Parent-owned RRD bytes enter a synchronous channel call, without transfer or
changing the payload/quality. Parent startup Promises are settled by primitive
notifications and rejected on disposal. Every channel is closed independently;
a late SDK startup completion is stopped again. Teardown severs references and
removes the iframe. There is still exactly one native renderer and clock,
no new playback UI, SDK patch or second backend. The live source path stays
direct upstream. Page-cache restoration starts a fresh viewer lifecycle.
The iframe relays only Escape and primitive pointer fields to parent-owned
events for the existing outer controls. Coordinates are translated by the
iframe bounds, including the native-chrome crop. Native Rerun still owns its
canvas gestures and divider; nothing is cancelled or sent back to the canvas.
The parent tracks the same divider for control alignment, not a second layout
or renderer. Teardown removes every relay listener and foreign frame reference.
The backend's ephemeral blueprint store has an exact viewport owner, explicit
release, idle expiry (300 seconds; reaped every 30 seconds), and shutdown cleanup.
An open owner renews every 30 seconds. Release is serialized with rendering;
late updates for that released identity are rejected. Expiry never stops an
active render, a recording, a Worker profile or a vehicle. Durable files are not
removed. The pre-existing 32-entry cache bound is unchanged; no new active data
or image-quality cap is introduced.
DOM disappearance is not memory acceptance. Compare process footprints before,
during and after ordinary sequential open/close cycles without forced GC or
page reload. See the dated memory-lifecycle report for measurements and limits.
Revision 2 and the input relay passed full typecheck, 740 frontend tests,
64 focused backend tests and a production build, served on canonical 8000.
Real browser acceptance of this revision remains pending: auto-review rejected
opening the heavy saved viewer under elevated pressure; explicit owner approval
was requested. This does not gate unrelated input/catalog contract work, nor
does a successful build prove that the recorded memory retention is fixed.

Some files were not shown because too many files have changed in this diff Show More