feat: add Polygon UI-0 run view

This commit is contained in:
DCCONSTRUCTIONS
2026-07-24 19:02:10 +03:00
parent a78c8c83f5
commit b790f29d59
18 changed files with 2193 additions and 43 deletions
+18 -1
View File
@@ -46,6 +46,7 @@ import type {
} from "./core/observation/sessionArchive";
import { useRecordedSessionAdmission } from "./core/observation/useRecordedSessionAdmission";
import { useWorkspaceLayoutProfile } from "./core/observation/useWorkspaceLayoutProfile";
import { resolvePolygonRunRoute } from "./core/polygon/runArchive";
import {
OBSERVATION_WORKSPACE_ID,
OBSERVATION_WORKSPACE_LAYOUT_VERSION,
@@ -149,12 +150,18 @@ function mergeViewerSettings(
export default function App() {
const runtime = useMissionRuntime();
const { selection } = useDevicePluginHost();
const polygonRunRoute = useMemo(
() => resolvePolygonRunRoute(typeof window === "undefined" ? "" : window.location.search),
[],
);
const workspace = useApplicationWorkspace<string>({
navigationOpen: false,
contentExpanded: true,
});
const [activeRoot, setActiveRoot] = useState<RootId | null>(null);
const [activeRoot, setActiveRoot] = useState<RootId | null>(
polygonRunRoute.active ? "system" : null,
);
const [sourceUrl, setSourceUrl] = useState("");
const [recordedReplay, setRecordedReplay] = useState<ObservationSessionReplayLaunch | null>(null);
const [recordedReplayLabel, setRecordedReplayLabel] = useState<string | null>(null);
@@ -187,6 +194,7 @@ export default function App() {
const replayActiveRef = useRef(false);
const sceneSettingsCommitterActiveRef = useRef(true);
const sceneSettingsCommitterRef = useRef<LatestAsyncCommitter<SceneSettings> | null>(null);
const polygonRunRouteOpenedRef = useRef(false);
const currentRoot = rootById(activeRoot);
const activeDefinition = workspaceById(workspace.activeView);
@@ -204,6 +212,12 @@ export default function App() {
runtimeUpdateViewerSettingsRef.current = runtime.updateViewerSettings;
replayActiveRef.current = replayActive;
useEffect(() => {
if (!polygonRunRoute.active || polygonRunRouteOpenedRef.current) return;
polygonRunRouteOpenedRef.current = true;
workspace.openView("polygon-run-internal");
}, [polygonRunRoute.active, workspace]);
if (!sceneSettingsCommitterRef.current) {
sceneSettingsCommitterRef.current = createLatestAsyncCommitter<SceneSettings>({
commit: (settings) => replayActiveRef.current
@@ -718,6 +732,8 @@ export default function App() {
</span>
) : null}
</div>
) : activeDefinition.kind === "polygon-run" ? (
<StatusBadge tone="accent">Только чтение</StatusBadge>
) : (
<StatusBadge tone="warning">Интерфейс готов</StatusBadge>
)
@@ -746,6 +762,7 @@ export default function App() {
livePerceptionLayers={livePerceptionLayers}
onLivePerceptionLayersChange={changeLivePerceptionLayers}
observationLayout={observationLayout}
polygonRunRoute={polygonRunRoute}
spatialControls={selection?.SpatialControlsView
? {
View: selection.SpatialControlsView,
@@ -0,0 +1,661 @@
export type PolygonRunState =
| "admitted"
| "starting"
| "running"
| "paused"
| "stopping"
| "completed"
| "failed"
| "aborted";
export interface PolygonRunSummary {
runId: string;
episodeId: string;
kind: string;
state: PolygonRunState;
createdAtUtc: string;
startedAtUtc: string | null;
endedAtUtc: string | null;
terminalReason: string | null;
scenarioGeneration: string;
profileGeneration: string;
missionCoreCommit: string;
hostProfileId: string;
reproducibilityTier: "R0" | "R1" | "R2";
clockDomain: string;
revision: number;
providerIds: string[];
artifactCount: number;
}
export interface PolygonProviderPin {
identifier: string;
version: string;
revision: string;
digest: string | null;
}
export interface PolygonAuthority {
generation: number;
commandTtlMaxNs: number;
heartbeatTimeoutMonotonicNs: number;
simulationOrShadowOnly: boolean;
actuatorAuthority: boolean;
navigationOrSafetyAccepted: boolean;
directActuatorSetpointsAllowed: boolean;
}
export interface PolygonRun extends PolygonRunSummary {
scenarioSha256: string;
profileSha256: string;
hostProfileSha256: string;
seed: number;
parentRunId: string | null;
providers: PolygonProviderPin[];
authority: PolygonAuthority;
}
export interface PolygonRunEvent {
runId: string;
sequence: number;
eventType: string;
observedAtUtc: string;
hostMonotonicNs: number;
simTimeNs: number | null;
payload: Record<string, unknown>;
}
export interface PolygonRunArtifact {
artifactId: string;
kind: string;
relativePath: string;
sha256: string;
byteLength: number;
sourceOfRecord: boolean;
sequence: number;
}
export interface PolygonRunCatalog {
items: PolygonRunSummary[];
total: number;
limitations: string[];
}
export interface PolygonRunDetail {
run: PolygonRun;
events: PolygonRunEvent[];
eventsTotal: number;
eventsTruncated: boolean;
commandCount: number;
artifacts: PolygonRunArtifact[];
limitations: string[];
}
export interface PolygonRunRoute {
active: boolean;
runId: string | null;
error: string | null;
}
export class PolygonRunContractError extends Error {}
export class PolygonRunApiError extends Error {
constructor(message: string, readonly status: number | null = null) {
super(message);
}
}
type PolygonRunFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const GIT_REVISION = /^[a-f0-9]{7,64}$/;
const ISO_WITH_TIMEZONE = /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/;
const RUN_STATES = new Set<PolygonRunState>([
"admitted",
"starting",
"running",
"paused",
"stopping",
"completed",
"failed",
"aborted",
]);
const REPRODUCIBILITY_TIERS = new Set(["R0", "R1", "R2"]);
const SUMMARY_KEYS = new Set([
"run_id",
"episode_id",
"kind",
"state",
"created_at_utc",
"started_at_utc",
"ended_at_utc",
"terminal_reason",
"scenario_generation",
"profile_generation",
"mission_core_commit",
"host_profile_id",
"reproducibility_tier",
"clock_domain",
"revision",
"provider_ids",
"artifact_count",
]);
const RUN_KEYS = new Set([
...SUMMARY_KEYS,
"scenario_sha256",
"profile_sha256",
"host_profile_sha256",
"seed",
"parent_run_id",
"providers",
"authority",
]);
const PROVIDER_KEYS = new Set(["identifier", "version", "revision", "digest"]);
const AUTHORITY_KEYS = new Set([
"generation",
"command_ttl_max_ns",
"heartbeat_timeout_monotonic_ns",
"simulation_or_shadow_only",
"actuator_authority",
"navigation_or_safety_accepted",
"direct_actuator_setpoints_allowed",
]);
const EVENT_KEYS = new Set([
"schema_version",
"run_id",
"sequence",
"event_type",
"observed_at_utc",
"host_monotonic_ns",
"sim_time_ns",
"payload",
]);
const ARTIFACT_KEYS = new Set([
"artifact_id",
"kind",
"relative_path",
"sha256",
"byte_length",
"source_of_record",
"sequence",
]);
const CATALOG_KEYS = new Set([
"schema_version",
"access",
"items",
"total",
"limitations",
]);
const DETAIL_KEYS = new Set([
"schema_version",
"access",
"run",
"events",
"events_total",
"events_truncated",
"commands",
"artifacts",
"limitations",
]);
const COMMAND_KEYS = new Set(["count", "content_exposed"]);
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function assertExactKeys(
value: Record<string, unknown>,
expected: ReadonlySet<string>,
label: string,
) {
const keys = Object.keys(value);
if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) {
throw new PolygonRunContractError(`${label} содержит неизвестные или отсутствующие поля.`);
}
}
function requireString(value: unknown, field: string, maximum = 512): string {
if (typeof value !== "string") {
throw new PolygonRunContractError(`Поле ${field} должно быть строкой.`);
}
const normalized = value.trim();
if (!normalized || normalized.length > maximum) {
throw new PolygonRunContractError(`Поле ${field} имеет недопустимую длину.`);
}
return normalized;
}
function requireId(value: unknown, field: string): string {
const identifier = requireString(value, field, 128);
if (!SAFE_ID.test(identifier)) {
throw new PolygonRunContractError(`Поле ${field} содержит небезопасный идентификатор.`);
}
return identifier;
}
function requireInteger(value: unknown, field: string, minimum = 0): number {
if (
typeof value !== "number" ||
!Number.isSafeInteger(value) ||
value < minimum
) {
throw new PolygonRunContractError(`Поле ${field} должно быть безопасным целым числом.`);
}
return value;
}
function requireTimestamp(value: unknown, field: string): string {
const timestamp = requireString(value, field, 64);
if (!ISO_WITH_TIMEZONE.test(timestamp) || !Number.isFinite(Date.parse(timestamp))) {
throw new PolygonRunContractError(`Поле ${field} должно быть ISO-датой с часовым поясом.`);
}
return timestamp;
}
function optionalTimestamp(value: unknown, field: string): string | null {
return value === null ? null : requireTimestamp(value, field);
}
function optionalString(value: unknown, field: string, maximum = 512): string | null {
return value === null ? null : requireString(value, field, maximum);
}
function requireBoolean(value: unknown, field: string): boolean {
if (typeof value !== "boolean") {
throw new PolygonRunContractError(`Поле ${field} должно быть boolean.`);
}
return value;
}
function requireSha256(value: unknown, field: string): string {
const digest = requireString(value, field, 64);
if (!SHA256.test(digest)) {
throw new PolygonRunContractError(`Поле ${field} должно содержать SHA-256.`);
}
return digest;
}
function decodeLimitations(value: unknown): string[] {
if (!Array.isArray(value) || value.length > 16) {
throw new PolygonRunContractError("Ограничения UI-0 должны быть ограниченным массивом.");
}
return value.map((entry, index) => requireString(entry, `limitations[${index}]`, 1_000));
}
function decodeSummary(
value: unknown,
expectedKeys: ReadonlySet<string> = SUMMARY_KEYS,
): PolygonRunSummary {
if (!isRecord(value)) {
throw new PolygonRunContractError("Сводка прогона должна быть объектом.");
}
assertExactKeys(value, expectedKeys, "Сводка прогона");
const stateValue = requireString(value.state, "state", 32);
if (!RUN_STATES.has(stateValue as PolygonRunState)) {
throw new PolygonRunContractError("Прогон содержит неизвестное состояние.");
}
const tier = requireString(value.reproducibility_tier, "reproducibility_tier", 2);
if (!REPRODUCIBILITY_TIERS.has(tier)) {
throw new PolygonRunContractError("Прогон содержит неизвестный уровень воспроизводимости.");
}
const commit = requireString(value.mission_core_commit, "mission_core_commit", 64);
if (!GIT_REVISION.test(commit)) {
throw new PolygonRunContractError("Прогон не содержит допустимую ревизию Mission Core.");
}
if (!Array.isArray(value.provider_ids) || value.provider_ids.length > 32) {
throw new PolygonRunContractError("Поле provider_ids должно быть ограниченным массивом.");
}
const providerIds = value.provider_ids.map((entry, index) =>
requireId(entry, `provider_ids[${index}]`));
if (new Set(providerIds).size !== providerIds.length) {
throw new PolygonRunContractError("Поле provider_ids содержит повторения.");
}
const startedAtUtc = optionalTimestamp(value.started_at_utc, "started_at_utc");
const endedAtUtc = optionalTimestamp(value.ended_at_utc, "ended_at_utc");
if (startedAtUtc && endedAtUtc && Date.parse(endedAtUtc) < Date.parse(startedAtUtc)) {
throw new PolygonRunContractError("Прогон завершился раньше времени запуска.");
}
return {
runId: requireId(value.run_id, "run_id"),
episodeId: requireId(value.episode_id, "episode_id"),
kind: requireId(value.kind, "kind"),
state: stateValue as PolygonRunState,
createdAtUtc: requireTimestamp(value.created_at_utc, "created_at_utc"),
startedAtUtc,
endedAtUtc,
terminalReason: optionalString(value.terminal_reason, "terminal_reason"),
scenarioGeneration: requireId(value.scenario_generation, "scenario_generation"),
profileGeneration: requireId(value.profile_generation, "profile_generation"),
missionCoreCommit: commit,
hostProfileId: requireId(value.host_profile_id, "host_profile_id"),
reproducibilityTier: tier as "R0" | "R1" | "R2",
clockDomain: requireString(value.clock_domain, "clock_domain", 128),
revision: requireInteger(value.revision, "revision"),
providerIds,
artifactCount: requireInteger(value.artifact_count, "artifact_count"),
};
}
function decodeProvider(value: unknown, index: number): PolygonProviderPin {
if (!isRecord(value)) {
throw new PolygonRunContractError(`providers[${index}] должен быть объектом.`);
}
assertExactKeys(value, PROVIDER_KEYS, `providers[${index}]`);
const digest = value.digest === null ? null : requireSha256(value.digest, `providers[${index}].digest`);
return {
identifier: requireId(value.identifier, `providers[${index}].identifier`),
version: requireString(value.version, `providers[${index}].version`, 128),
revision: requireString(value.revision, `providers[${index}].revision`, 128),
digest,
};
}
function decodeAuthority(value: unknown): PolygonAuthority {
if (!isRecord(value)) {
throw new PolygonRunContractError("authority должен быть объектом.");
}
assertExactKeys(value, AUTHORITY_KEYS, "authority");
return {
generation: requireInteger(value.generation, "authority.generation", 1),
commandTtlMaxNs: requireInteger(value.command_ttl_max_ns, "authority.command_ttl_max_ns", 1),
heartbeatTimeoutMonotonicNs: requireInteger(
value.heartbeat_timeout_monotonic_ns,
"authority.heartbeat_timeout_monotonic_ns",
1,
),
simulationOrShadowOnly: requireBoolean(
value.simulation_or_shadow_only,
"authority.simulation_or_shadow_only",
),
actuatorAuthority: requireBoolean(value.actuator_authority, "authority.actuator_authority"),
navigationOrSafetyAccepted: requireBoolean(
value.navigation_or_safety_accepted,
"authority.navigation_or_safety_accepted",
),
directActuatorSetpointsAllowed: requireBoolean(
value.direct_actuator_setpoints_allowed,
"authority.direct_actuator_setpoints_allowed",
),
};
}
function decodeRun(value: unknown): PolygonRun {
if (!isRecord(value)) {
throw new PolygonRunContractError("Прогон должен быть объектом.");
}
const summary = decodeSummary(value, RUN_KEYS);
if (!Array.isArray(value.providers) || value.providers.length > 32) {
throw new PolygonRunContractError("providers должен быть ограниченным массивом.");
}
const providers = value.providers.map(decodeProvider);
if (
providers.length !== summary.providerIds.length ||
providers.some((provider, index) => provider.identifier !== summary.providerIds[index])
) {
throw new PolygonRunContractError("Провайдеры прогона не совпадают со сводкой.");
}
const parentRunId = value.parent_run_id === null
? null
: requireId(value.parent_run_id, "parent_run_id");
return {
...summary,
scenarioSha256: requireSha256(value.scenario_sha256, "scenario_sha256"),
profileSha256: requireSha256(value.profile_sha256, "profile_sha256"),
hostProfileSha256: requireSha256(value.host_profile_sha256, "host_profile_sha256"),
seed: requireInteger(value.seed, "seed"),
parentRunId,
providers,
authority: decodeAuthority(value.authority),
};
}
function decodeEvent(value: unknown, index: number, runId: string): PolygonRunEvent {
if (!isRecord(value)) {
throw new PolygonRunContractError(`events[${index}] должен быть объектом.`);
}
assertExactKeys(value, EVENT_KEYS, `events[${index}]`);
if (value.schema_version !== "missioncore.qualification-event/v1") {
throw new PolygonRunContractError(`events[${index}] содержит неизвестную схему.`);
}
if (!isRecord(value.payload)) {
throw new PolygonRunContractError(`events[${index}].payload должен быть объектом.`);
}
const eventRunId = requireId(value.run_id, `events[${index}].run_id`);
if (eventRunId !== runId) {
throw new PolygonRunContractError(`events[${index}] относится к другому прогону.`);
}
return {
runId: eventRunId,
sequence: requireInteger(value.sequence, `events[${index}].sequence`, 1),
eventType: requireId(value.event_type, `events[${index}].event_type`),
observedAtUtc: requireTimestamp(value.observed_at_utc, `events[${index}].observed_at_utc`),
hostMonotonicNs: requireInteger(
value.host_monotonic_ns,
`events[${index}].host_monotonic_ns`,
),
simTimeNs: value.sim_time_ns === null
? null
: requireInteger(value.sim_time_ns, `events[${index}].sim_time_ns`),
payload: value.payload,
};
}
function decodeArtifact(value: unknown, index: number): PolygonRunArtifact {
if (!isRecord(value)) {
throw new PolygonRunContractError(`artifacts[${index}] должен быть объектом.`);
}
assertExactKeys(value, ARTIFACT_KEYS, `artifacts[${index}]`);
const relativePath = requireString(value.relative_path, `artifacts[${index}].relative_path`, 512);
const segments = relativePath.split("/");
if (
relativePath.startsWith("/") ||
relativePath.includes("\\") ||
segments.some((segment) => !segment || segment === "." || segment === "..")
) {
throw new PolygonRunContractError(`artifacts[${index}] содержит небезопасный путь.`);
}
return {
artifactId: requireId(value.artifact_id, `artifacts[${index}].artifact_id`),
kind: requireId(value.kind, `artifacts[${index}].kind`),
relativePath,
sha256: requireSha256(value.sha256, `artifacts[${index}].sha256`),
byteLength: requireInteger(value.byte_length, `artifacts[${index}].byte_length`),
sourceOfRecord: requireBoolean(
value.source_of_record,
`artifacts[${index}].source_of_record`,
),
sequence: requireInteger(value.sequence, `artifacts[${index}].sequence`, 1),
};
}
export function decodePolygonRunCatalog(payload: unknown): PolygonRunCatalog {
if (!isRecord(payload)) {
throw new PolygonRunContractError("Каталог прогонов должен быть объектом.");
}
assertExactKeys(payload, CATALOG_KEYS, "Каталог прогонов");
if (
payload.schema_version !== "missioncore.polygon-run-catalog/v1" ||
payload.access !== "read-only"
) {
throw new PolygonRunContractError("Каталог не является поддерживаемым read-only контрактом.");
}
if (!Array.isArray(payload.items) || payload.items.length > 100) {
throw new PolygonRunContractError("Каталог должен содержать ограниченный массив items.");
}
const items = payload.items.map((item) => decodeSummary(item));
if (new Set(items.map(({ runId }) => runId)).size !== items.length) {
throw new PolygonRunContractError("Каталог содержит повторяющиеся прогоны.");
}
const total = requireInteger(payload.total, "total");
if (total < items.length) {
throw new PolygonRunContractError("Размер каталога меньше числа возвращённых прогонов.");
}
return {
items,
total,
limitations: decodeLimitations(payload.limitations),
};
}
export function decodePolygonRunDetail(payload: unknown): PolygonRunDetail {
if (!isRecord(payload)) {
throw new PolygonRunContractError("Детали прогона должны быть объектом.");
}
assertExactKeys(payload, DETAIL_KEYS, "Детали прогона");
if (
payload.schema_version !== "missioncore.polygon-run-detail/v1" ||
payload.access !== "read-only"
) {
throw new PolygonRunContractError("Детали не являются поддерживаемым read-only контрактом.");
}
const run = decodeRun(payload.run);
if (!Array.isArray(payload.events) || payload.events.length > 500) {
throw new PolygonRunContractError("Журнал событий должен быть ограниченным массивом.");
}
const events = payload.events.map((event, index) => decodeEvent(event, index, run.runId));
for (let index = 1; index < events.length; index += 1) {
if (events[index].sequence !== events[index - 1].sequence + 1) {
throw new PolygonRunContractError("Возвращённый фрагмент событий не является непрерывным.");
}
}
const eventsTotal = requireInteger(payload.events_total, "events_total");
const eventsTruncated = requireBoolean(payload.events_truncated, "events_truncated");
if (eventsTotal < events.length || eventsTruncated !== (eventsTotal !== events.length)) {
throw new PolygonRunContractError("Метаданные усечения событий противоречат журналу.");
}
if (!isRecord(payload.commands)) {
throw new PolygonRunContractError("commands должен быть объектом.");
}
assertExactKeys(payload.commands, COMMAND_KEYS, "commands");
if (payload.commands.content_exposed !== false) {
throw new PolygonRunContractError("UI-0 не принимает содержимое команд.");
}
if (!Array.isArray(payload.artifacts) || payload.artifacts.length > 500) {
throw new PolygonRunContractError("Артефакты должны быть ограниченным массивом.");
}
const artifacts = payload.artifacts.map(decodeArtifact);
return {
run,
events,
eventsTotal,
eventsTruncated,
commandCount: requireInteger(payload.commands.count, "commands.count"),
artifacts,
limitations: decodeLimitations(payload.limitations),
};
}
export function resolvePolygonRunRoute(search: string): PolygonRunRoute {
const parameters = new URLSearchParams(search);
const workspaceValues = parameters.getAll("workspace");
if (workspaceValues.length !== 1 || workspaceValues[0] !== "polygon-run") {
return { active: false, runId: null, error: null };
}
const runValues = parameters.getAll("run");
if (runValues.length === 0) {
return { active: true, runId: null, error: null };
}
if (runValues.length !== 1 || !SAFE_ID.test(runValues[0])) {
return {
active: true,
runId: null,
error: "Прямая ссылка содержит некорректный идентификатор прогона.",
};
}
return { active: true, runId: runValues[0], error: null };
}
async function responseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.toLowerCase().startsWith("application/json")) {
throw new PolygonRunContractError("Polygon API вернул ответ не в формате JSON.");
}
try {
return await response.json();
} catch {
throw new PolygonRunContractError("Polygon API вернул повреждённый JSON.");
}
}
function apiErrorMessage(body: unknown, fallback: string): string {
if (!isRecord(body) || typeof body.detail !== "string") return fallback;
const detail = body.detail.trim();
return detail && detail.length <= 1_000 ? detail : fallback;
}
async function getJson(
url: string,
fallback: string,
signal: AbortSignal | undefined,
fetcher: PolygonRunFetch,
): Promise<unknown> {
let response: Response;
try {
response = await fetcher(url, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
throw new PolygonRunApiError(fallback);
}
const body = await responseBody(response);
if (!response.ok) {
throw new PolygonRunApiError(
apiErrorMessage(body, `${fallback} HTTP ${response.status}.`),
response.status,
);
}
return body;
}
export async function fetchPolygonRunCatalog({
signal,
limit = 20,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
limit?: number;
fetcher?: PolygonRunFetch;
} = {}): Promise<PolygonRunCatalog> {
const admittedLimit = Number.isInteger(limit) && limit >= 1 && limit <= 100 ? limit : 20;
const body = await getJson(
`/api/v1/polygon/runs?limit=${admittedLimit}`,
"Не удалось загрузить каталог прогонов Полигона.",
signal,
fetcher,
);
return decodePolygonRunCatalog(body);
}
export async function fetchPolygonRunDetail(
runId: string,
{
signal,
eventLimit = 200,
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
eventLimit?: number;
fetcher?: PolygonRunFetch;
} = {},
): Promise<PolygonRunDetail> {
if (!SAFE_ID.test(runId)) {
throw new PolygonRunContractError("Идентификатор прогона имеет недопустимый формат.");
}
const admittedLimit = Number.isInteger(eventLimit) && eventLimit >= 1 && eventLimit <= 500
? eventLimit
: 200;
const body = await getJson(
`/api/v1/polygon/runs/${encodeURIComponent(runId)}?event_limit=${admittedLimit}`,
"Не удалось загрузить доказательства прогона.",
signal,
fetcher,
);
return decodePolygonRunDetail(body);
}
+18 -2
View File
@@ -10,7 +10,8 @@ export type WorkspaceKind =
| "map"
| "timeline"
| "missions"
| "catalog";
| "catalog"
| "polygon-run";
export type CapabilityStatus = "active" | "ready" | "contract" | "later";
@@ -35,6 +36,7 @@ export interface WorkspaceDefinition {
description: string;
icon: IconName;
kind: WorkspaceKind;
internalOnly?: boolean;
groups: CapabilityGroup[];
}
@@ -130,6 +132,18 @@ export const roots: RootDefinition[] = [
];
export const workspaces: WorkspaceDefinition[] = [
{
id: "polygon-run-internal",
root: "system",
label: "Прогон Полигона",
title: "Прогон Полигона",
eyebrow: "ПОЛИГОН / UI-0",
description: "Read-only доказательства квалификационного прогона PX4/Gazebo.",
icon: "activity",
kind: "polygon-run",
internalOnly: true,
groups: [],
},
{
id: "command-overview",
root: "center",
@@ -747,7 +761,9 @@ export function workspaceById(id: string | null): WorkspaceDefinition | null {
}
export function workspacesForRoot(root: RootId | null): WorkspaceDefinition[] {
return root ? workspaces.filter((workspace) => workspace.root === root) : [];
return root
? workspaces.filter((workspace) => workspace.root === root && !workspace.internalOnly)
: [];
}
export const capabilityStatusLabel: Record<CapabilityStatus, string> = {
+26 -2
View File
@@ -10,10 +10,16 @@
@media (max-width: 1280px) {
.overview-grid,
.mission-layout {
.mission-layout,
.polygon-run-layout,
.polygon-run-evidence-grid {
grid-template-columns: 1fr;
}
.polygon-run-providers {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.pipeline-strip {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@@ -67,6 +73,10 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.polygon-run-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.workspace-lead {
align-items: flex-start;
flex-direction: column;
@@ -131,10 +141,24 @@
.metrics-grid,
.capability-summary,
.camera-grid {
.camera-grid,
.polygon-run-metrics,
.polygon-run-providers {
grid-template-columns: 1fr;
}
.polygon-run-identity dl {
grid-template-columns: 1fr;
}
.polygon-run-event-list article {
grid-template-columns: 2.3rem minmax(0, 1fr);
}
.polygon-run-event-list article > span:last-child {
grid-column: 2;
}
.camera-grid {
grid-template-rows: repeat(4, minmax(14rem, 1fr));
}
@@ -90,6 +90,399 @@
line-height: 1.4;
}
.polygon-run-workspace {
gap: 0.85rem;
}
.polygon-run-message {
display: grid;
min-height: 18rem;
place-items: start;
align-content: center;
gap: 0.8rem;
}
.polygon-run-message h2,
.polygon-run-message p,
.polygon-run-panel-heading h3,
.polygon-run-artifacts h3,
.polygon-run-limitations h3 {
margin: 0;
}
.polygon-run-message h2 {
color: var(--nodedc-text-primary);
font-size: 1.35rem;
letter-spacing: -0.035em;
}
.polygon-run-message p {
max-width: 38rem;
color: var(--nodedc-text-muted);
font-size: 0.72rem;
line-height: 1.55;
}
.polygon-run-lead-status {
display: grid;
justify-items: end;
gap: 0.45rem;
}
.polygon-run-lead-status > span {
color: var(--nodedc-text-muted);
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 0.58rem;
}
.polygon-run-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.65rem;
}
.polygon-run-metrics > div {
display: grid;
min-height: 5.5rem;
align-content: space-between;
gap: 0.32rem;
border-radius: 1rem;
background: var(--station-panel);
padding: 0.9rem 1rem;
}
.polygon-run-metrics span,
.polygon-run-metrics small,
.polygon-run-panel-heading > span {
color: var(--nodedc-text-muted);
font-size: 0.6rem;
}
.polygon-run-metrics strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 1.05rem;
font-weight: 680;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-metrics small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-layout,
.polygon-run-evidence-grid {
display: grid;
grid-template-columns: minmax(17rem, 0.72fr) minmax(0, 1.28fr);
gap: 0.75rem;
}
.polygon-run-panel-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.polygon-run-panel-heading h3,
.polygon-run-artifacts h3,
.polygon-run-limitations h3 {
margin-top: 0.4rem;
color: var(--nodedc-text-primary);
font-size: 0.94rem;
letter-spacing: -0.025em;
}
.polygon-run-list {
display: grid;
gap: 0.35rem;
}
.polygon-run-list button {
display: grid;
min-width: 0;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 0.65rem;
border: 0;
border-radius: 0.8rem;
background: rgb(255 255 255 / 0.025);
padding: 0.65rem 0.7rem;
text-align: left;
cursor: pointer;
}
.polygon-run-list button:hover,
.polygon-run-list button:focus-visible,
.polygon-run-list button[data-active="true"] {
outline: 0;
background: rgb(255 255 255 / 0.07);
}
.polygon-run-list button > i,
.polygon-run-providers i {
width: 0.48rem;
height: 0.48rem;
border-radius: 50%;
background: var(--nodedc-text-muted);
}
.polygon-run-list button > i[data-state="completed"],
.polygon-run-providers i {
background: rgb(var(--nodedc-success-rgb));
}
.polygon-run-list button > i[data-state="failed"],
.polygon-run-list button > i[data-state="aborted"] {
background: rgb(var(--nodedc-danger-rgb));
}
.polygon-run-list button > i[data-state="running"] {
background: var(--nodedc-text-primary);
}
.polygon-run-list button > span {
display: grid;
min-width: 0;
gap: 0.18rem;
}
.polygon-run-list strong,
.polygon-run-list small,
.polygon-run-list em {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-list strong {
color: var(--nodedc-text-primary);
font-size: 0.66rem;
}
.polygon-run-list small,
.polygon-run-list em {
color: var(--nodedc-text-muted);
font-size: 0.56rem;
}
.polygon-run-list em {
font-style: normal;
}
.polygon-run-identity dl {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.3rem 1rem;
margin: 0.9rem 0 0;
}
.polygon-run-identity dl > div {
display: grid;
min-width: 0;
grid-template-columns: 7rem minmax(0, 1fr);
gap: 0.7rem;
border-bottom: 1px solid var(--station-hairline);
padding: 0.48rem 0;
}
.polygon-run-identity dt,
.polygon-run-identity dd {
overflow: hidden;
margin: 0;
font-size: 0.61rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-identity dt {
color: var(--nodedc-text-muted);
}
.polygon-run-identity dd {
color: var(--nodedc-text-primary);
}
.polygon-run-safety-boundary {
display: grid;
gap: 0.45rem;
margin-top: 1rem;
border-radius: 0.8rem;
background: rgb(255 255 255 / 0.035);
padding: 0.75rem;
}
.polygon-run-safety-boundary p {
margin: 0;
color: var(--nodedc-text-muted);
font-size: 0.59rem;
line-height: 1.5;
}
.polygon-run-providers {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.55rem;
}
.polygon-run-providers > div {
display: grid;
min-width: 0;
grid-template-columns: auto minmax(0, 1fr);
gap: 0.22rem 0.55rem;
border-radius: 0.85rem;
background: var(--station-panel);
padding: 0.72rem;
}
.polygon-run-providers i {
grid-row: 1 / 3;
align-self: center;
}
.polygon-run-providers span {
display: grid;
min-width: 0;
gap: 0.15rem;
}
.polygon-run-providers strong,
.polygon-run-providers small,
.polygon-run-providers code {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-providers strong {
color: var(--nodedc-text-primary);
font-size: 0.64rem;
}
.polygon-run-providers small,
.polygon-run-providers code {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.polygon-run-providers code {
grid-column: 2;
}
.polygon-run-event-list {
display: grid;
}
.polygon-run-event-list article {
display: grid;
min-width: 0;
grid-template-columns: 2.3rem minmax(0, 1fr) auto;
align-items: start;
gap: 0.8rem;
border-top: 1px solid var(--station-hairline);
padding: 0.75rem 0;
}
.polygon-run-event-list article:first-child {
border-top: 0;
}
.polygon-run-event-list article > div {
display: grid;
min-width: 0;
gap: 0.2rem;
}
.polygon-run-event-list strong {
color: var(--nodedc-text-primary);
font-size: 0.66rem;
}
.polygon-run-event-list small,
.polygon-run-event-list article > span:last-child,
.polygon-run-event-sequence {
color: var(--nodedc-text-muted);
font-size: 0.55rem;
}
.polygon-run-event-list code {
overflow: hidden;
color: var(--nodedc-text-secondary);
font-size: 0.56rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-event-sequence {
border-radius: 0.4rem;
background: rgb(255 255 255 / 0.04);
padding: 0.2rem 0.3rem;
text-align: center;
}
.polygon-run-artifacts > div {
display: grid;
gap: 0.35rem;
margin-top: 0.85rem;
}
.polygon-run-artifacts article {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.025);
padding: 0.65rem;
}
.polygon-run-artifacts article > span {
display: grid;
min-width: 0;
gap: 0.18rem;
}
.polygon-run-artifacts strong,
.polygon-run-artifacts small,
.polygon-run-artifacts code {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.polygon-run-artifacts strong {
color: var(--nodedc-text-primary);
font-size: 0.62rem;
}
.polygon-run-artifacts small,
.polygon-run-artifacts code,
.polygon-run-artifacts p,
.polygon-run-limitations li {
color: var(--nodedc-text-muted);
font-size: 0.56rem;
}
.polygon-run-artifacts p {
margin: 0.9rem 0 0;
}
.polygon-run-limitations ul {
display: grid;
gap: 0.55rem;
margin: 0.85rem 0 0;
padding-left: 1rem;
}
.polygon-run-limitations li {
line-height: 1.5;
}
.capability-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -0,0 +1,322 @@
import { useEffect, useMemo, useState } from "react";
import {
Button,
GlassSurface,
StatusBadge,
} from "@nodedc/ui-react";
import {
fetchPolygonRunCatalog,
fetchPolygonRunDetail,
type PolygonRunCatalog,
type PolygonRunDetail,
type PolygonRunRoute,
type PolygonRunState,
} from "../core/polygon/runArchive";
interface PolygonRunWorkspaceProps {
route: PolygonRunRoute;
}
const stateLabels: Record<PolygonRunState, string> = {
admitted: "Допущен",
starting: "Запускается",
running: "Выполняется",
paused: "На паузе",
stopping: "Останавливается",
completed: "Завершён",
failed: "Ошибка",
aborted: "Прерван",
};
function stateTone(
state: PolygonRunState,
): "success" | "accent" | "warning" | "danger" | "neutral" {
if (state === "completed") return "success";
if (state === "running") return "accent";
if (state === "failed" || state === "aborted") return "danger";
if (state === "starting" || state === "stopping" || state === "paused") return "warning";
return "neutral";
}
function formatTimestamp(value: string | null): string {
if (!value) return "—";
return new Intl.DateTimeFormat("ru-RU", {
dateStyle: "medium",
timeStyle: "medium",
}).format(new Date(value));
}
function formatBytes(value: number): string {
if (value === 0) return "0 Б";
const units = ["Б", "КБ", "МБ", "ГБ"];
const exponent = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
return `${(value / 1024 ** exponent).toLocaleString("ru-RU", {
maximumFractionDigits: 1,
})} ${units[exponent]}`;
}
function errorMessage(error: unknown): string {
if (error instanceof Error && error.message.trim()) return error.message;
return "Не удалось прочитать доказательства прогона.";
}
export function PolygonRunWorkspace({ route }: PolygonRunWorkspaceProps) {
const [catalog, setCatalog] = useState<PolygonRunCatalog | null>(null);
const [detail, setDetail] = useState<PolygonRunDetail | null>(null);
const [selectedRunId, setSelectedRunId] = useState<string | null>(route.runId);
const [loading, setLoading] = useState(route.error === null);
const [error, setError] = useState<string | null>(route.error);
const [reloadGeneration, setReloadGeneration] = useState(0);
useEffect(() => {
if (route.error) {
setLoading(false);
setError(route.error);
return;
}
const controller = new AbortController();
setLoading(true);
setError(null);
void (async () => {
try {
const nextCatalog = await fetchPolygonRunCatalog({ signal: controller.signal });
if (controller.signal.aborted) return;
setCatalog(nextCatalog);
const targetRunId = selectedRunId ?? route.runId ?? nextCatalog.items[0]?.runId ?? null;
if (!targetRunId) {
setDetail(null);
return;
}
const nextDetail = await fetchPolygonRunDetail(targetRunId, {
signal: controller.signal,
});
if (controller.signal.aborted) return;
setSelectedRunId(targetRunId);
setDetail(nextDetail);
} catch (loadError) {
if (controller.signal.aborted) return;
setDetail(null);
setError(errorMessage(loadError));
} finally {
if (!controller.signal.aborted) setLoading(false);
}
})();
return () => controller.abort();
}, [reloadGeneration, route.error, route.runId, selectedRunId]);
const visibleEvents = useMemo(
() => detail ? [...detail.events].reverse() : [],
[detail],
);
if (loading && !detail) {
return (
<div className="standard-workspace polygon-run-workspace">
<GlassSurface className="polygon-run-message" padding="lg">
<StatusBadge tone="accent">Только чтение</StatusBadge>
<h2>Проверяем журнал прогона</h2>
<p>Mission Core читает манифест, события и индекс артефактов без запуска провайдеров.</p>
</GlassSurface>
</div>
);
}
if (error) {
return (
<div className="standard-workspace polygon-run-workspace">
<GlassSurface className="polygon-run-message" padding="lg">
<StatusBadge tone="danger">Данные недоступны</StatusBadge>
<h2>UI-0 не может открыть прогон</h2>
<p>{error}</p>
<Button
size="compact"
variant="secondary"
onClick={() => setReloadGeneration((value) => value + 1)}
>
Повторить чтение
</Button>
</GlassSurface>
</div>
);
}
if (!detail) {
return (
<div className="standard-workspace polygon-run-workspace">
<GlassSurface className="polygon-run-message" padding="lg">
<StatusBadge tone="neutral">Журнал пуст</StatusBadge>
<h2>Квалификационных прогонов пока нет</h2>
<p>Экран появится автоматически после публикации первого журнала в read-only источник.</p>
</GlassSurface>
</div>
);
}
const { run } = detail;
return (
<div className="standard-workspace polygon-run-workspace">
<section className="workspace-lead workspace-lead--compact">
<div>
<span className="section-eyebrow">ПОЛИГОН / КВАЛИФИКАЦИОННЫЙ ПРОГОН</span>
<h2>{run.runId}</h2>
<p>
Канонический журнал Mission Core. Экран не содержит lifecycle-операций,
команд управления или доступа к физическим актуаторам.
</p>
</div>
<div className="polygon-run-lead-status">
<StatusBadge tone={stateTone(run.state)}>{stateLabels[run.state]}</StatusBadge>
<span>read-only · {run.reproducibilityTier}</span>
</div>
</section>
<section className="polygon-run-metrics" aria-label="Сводка прогона">
<div>
<span>Состояние</span>
<strong>{stateLabels[run.state]}</strong>
<small>{run.terminalReason ?? "терминальная причина отсутствует"}</small>
</div>
<div>
<span>Провайдеры</span>
<strong>{run.providers.length}</strong>
<small>{run.providerIds.join(" · ")}</small>
</div>
<div>
<span>События</span>
<strong>{detail.eventsTotal}</strong>
<small>{detail.eventsTruncated ? "показан последний фрагмент" : "журнал целиком"}</small>
</div>
<div>
<span>Команды</span>
<strong>{detail.commandCount}</strong>
<small>содержимое не публикуется UI-0</small>
</div>
</section>
<div className="polygon-run-layout">
<GlassSurface className="polygon-run-catalog" padding="lg">
<header className="polygon-run-panel-heading">
<div>
<span className="section-eyebrow">ПОСЛЕДНИЕ ПРОГОНЫ</span>
<h3>{catalog?.total ?? 0} в источнике</h3>
</div>
<Button
size="compact"
variant="secondary"
onClick={() => setReloadGeneration((value) => value + 1)}
>
Обновить
</Button>
</header>
<div className="polygon-run-list">
{catalog?.items.map((item) => (
<button
type="button"
key={item.runId}
data-active={item.runId === run.runId ? "true" : undefined}
onClick={() => setSelectedRunId(item.runId)}
>
<i data-state={item.state} aria-hidden="true" />
<span>
<strong>{item.runId}</strong>
<small>{formatTimestamp(item.createdAtUtc)}</small>
</span>
<em>{stateLabels[item.state]}</em>
</button>
))}
</div>
</GlassSurface>
<GlassSurface className="polygon-run-identity" padding="lg">
<span className="section-eyebrow">ИДЕНТИЧНОСТЬ И ГРАНИЦА</span>
<dl>
<div><dt>Сценарий</dt><dd>{run.scenarioGeneration}</dd></div>
<div><dt>Профиль</dt><dd>{run.profileGeneration}</dd></div>
<div><dt>Host profile</dt><dd>{run.hostProfileId}</dd></div>
<div><dt>Mission Core</dt><dd><code>{run.missionCoreCommit.slice(0, 12)}</code></dd></div>
<div><dt>Clock</dt><dd><code>{run.clockDomain}</code></dd></div>
<div><dt>Seed</dt><dd>{run.seed}</dd></div>
<div><dt>Начало</dt><dd>{formatTimestamp(run.startedAtUtc)}</dd></div>
<div><dt>Завершение</dt><dd>{formatTimestamp(run.endedAtUtc)}</dd></div>
</dl>
<div className="polygon-run-safety-boundary">
<StatusBadge tone="success">Virtual only</StatusBadge>
<p>
Actuator authority: {run.authority.actuatorAuthority ? "да" : "нет"} ·
direct setpoints: {run.authority.directActuatorSetpointsAllowed ? "да" : "нет"} ·
navigation/safety accepted: {run.authority.navigationOrSafetyAccepted ? "да" : "нет"}
</p>
</div>
</GlassSurface>
</div>
<section className="polygon-run-providers" aria-label="Провайдеры прогона">
{run.providers.map((provider) => (
<div key={provider.identifier}>
<i aria-hidden="true" />
<span>
<strong>{provider.identifier}</strong>
<small>{provider.version}</small>
</span>
<code>{provider.revision.slice(0, 16)}</code>
</div>
))}
</section>
<GlassSurface className="polygon-run-events" padding="lg">
<header className="polygon-run-panel-heading">
<div>
<span className="section-eyebrow">ЖУРНАЛ СОБЫТИЙ</span>
<h3>Последние переходы и факты</h3>
</div>
<span>revision {run.revision}</span>
</header>
<div className="polygon-run-event-list">
{visibleEvents.map((event) => (
<article key={event.sequence}>
<span className="polygon-run-event-sequence">
{String(event.sequence).padStart(3, "0")}
</span>
<div>
<strong>{event.eventType}</strong>
<small>{formatTimestamp(event.observedAtUtc)}</small>
<code>{JSON.stringify(event.payload)}</code>
</div>
<span>{event.simTimeNs === null ? "host" : `${event.simTimeNs} ns`}</span>
</article>
))}
</div>
</GlassSurface>
<div className="polygon-run-evidence-grid">
<GlassSurface className="polygon-run-artifacts" padding="lg">
<span className="section-eyebrow">АРТЕФАКТЫ</span>
<h3>{detail.artifacts.length || run.artifactCount} ссылок в индексе</h3>
{detail.artifacts.length ? (
<div>
{detail.artifacts.map((artifact) => (
<article key={artifact.artifactId}>
<span>
<strong>{artifact.kind}</strong>
<small>{artifact.relativePath}</small>
</span>
<code>{artifact.sha256.slice(0, 12)} · {formatBytes(artifact.byteLength)}</code>
</article>
))}
</div>
) : (
<p>В журнале прогона нет зарегистрированных artifact-index записей.</p>
)}
</GlassSurface>
<GlassSurface className="polygon-run-limitations" padding="lg">
<span className="section-eyebrow">ЧЕСТНАЯ ГРАНИЦА UI-0</span>
<h3>Что этот результат ещё не доказывает</h3>
<ul>
{detail.limitations.map((limitation) => <li key={limitation}>{limitation}</li>)}
</ul>
</GlassSurface>
</div>
</div>
);
}
@@ -22,6 +22,7 @@ import {
import { ObservationTimeline } from "../components/ObservationTimeline";
import { FloatingObservationWindow } from "../components/FloatingObservationWindow";
import type { ObservationSessionReplayLaunch } from "../core/observation/sessionArchive";
import type { PolygonRunRoute } from "../core/polygon/runArchive";
import type { RecordedSessionAdmissionController } from "../core/observation/useRecordedSessionAdmission";
import type {
RecordedAdmissionPhase,
@@ -55,6 +56,7 @@ import {
} from "../productModel";
import { finiteMetric, formatNumber, pipelineLatency, sourceModeLabel } from "../presentation";
import type { SceneSettings } from "../sceneSettings";
import { PolygonRunWorkspace } from "./PolygonRunWorkspace";
function statusTone(status: CapabilityStatus): "success" | "accent" | "warning" | "neutral" {
if (status === "active") return "success";
@@ -136,6 +138,7 @@ export interface WorkspaceRendererProps {
cuboids3d: boolean;
}) => void;
observationLayout: ObservationLayoutController;
polygonRunRoute: PolygonRunRoute;
navigation: WorkspaceNavigation;
spatialControls: {
View: ComponentType<DevicePluginConnectionProps>;
@@ -1303,6 +1306,8 @@ export function WorkspaceRenderer(props: WorkspaceRendererProps) {
return <MissionWorkspace {...props} />;
case "catalog":
return <CatalogWorkspace {...props} />;
case "polygon-run":
return <PolygonRunWorkspace route={props.polygonRunRoute} />;
case "device":
return null;
}
@@ -0,0 +1,230 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let decodePolygonRunCatalog;
let decodePolygonRunDetail;
let fetchPolygonRunCatalog;
let fetchPolygonRunDetail;
let resolvePolygonRunRoute;
let PolygonRunContractError;
let workspaceById;
let workspacesForRoot;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
decodePolygonRunCatalog,
decodePolygonRunDetail,
fetchPolygonRunCatalog,
fetchPolygonRunDetail,
resolvePolygonRunRoute,
PolygonRunContractError,
} = await server.ssrLoadModule("/src/core/polygon/runArchive.ts"));
({ workspaceById, workspacesForRoot } = await server.ssrLoadModule("/src/productModel.ts"));
});
after(async () => {
await server?.close();
});
function summary(overrides = {}) {
return {
run_id: "s1b-6cb1495-20260724t1535z",
episode_id: "episode-s1b-6cb1495-20260724t1535z",
kind: "simulation_closed_loop",
state: "completed",
created_at_utc: "2026-07-24T15:35:00Z",
started_at_utc: "2026-07-24T15:35:02Z",
ended_at_utc: "2026-07-24T15:35:05Z",
terminal_reason: "operator-stop-clean",
scenario_generation: "px4-v1.17.0-stock-rover-ackermann",
profile_generation: "stock-rover-lifecycle-v1",
mission_core_commit: "6cb1495a1234567890abcdef1234567890abcdef",
host_profile_id: "mission-gpu-s0",
reproducibility_tier: "R1",
clock_domain: "gazebo:/clock",
revision: 5,
provider_ids: ["px4-autopilot", "gazebo"],
artifact_count: 1,
...overrides,
};
}
function catalog(overrides = {}) {
return {
schema_version: "missioncore.polygon-run-catalog/v1",
access: "read-only",
items: [summary()],
total: 1,
limitations: ["Qualification evidence only."],
...overrides,
};
}
function detail(overrides = {}) {
return {
schema_version: "missioncore.polygon-run-detail/v1",
access: "read-only",
run: {
...summary(),
scenario_sha256: "a".repeat(64),
profile_sha256: "b".repeat(64),
host_profile_sha256: "c".repeat(64),
seed: 42,
parent_run_id: null,
providers: [
{
identifier: "px4-autopilot",
version: "v1.17.0",
revision: "v1.17.0",
digest: null,
},
{
identifier: "gazebo",
version: "harmonic",
revision: "8.9.0",
digest: null,
},
],
authority: {
generation: 1,
command_ttl_max_ns: 250_000_000,
heartbeat_timeout_monotonic_ns: 500_000_000,
simulation_or_shadow_only: true,
actuator_authority: false,
navigation_or_safety_accepted: false,
direct_actuator_setpoints_allowed: false,
},
},
events: [
{
schema_version: "missioncore.qualification-event/v1",
run_id: "s1b-6cb1495-20260724t1535z",
sequence: 5,
event_type: "lifecycle.state-changed",
observed_at_utc: "2026-07-24T15:35:05Z",
host_monotonic_ns: 5,
sim_time_ns: 2_000_000,
payload: {
from: "stopping",
to: "completed",
reason: "operator-stop-clean",
},
},
],
events_total: 1,
events_truncated: false,
commands: {
count: 0,
content_exposed: false,
},
artifacts: [
{
artifact_id: "provider-log-index",
kind: "log-index",
relative_path: "provider-runtime/processes.json",
sha256: "a".repeat(64),
byte_length: 512,
source_of_record: true,
sequence: 1,
},
],
limitations: ["Qualification evidence only."],
...overrides,
};
}
function jsonResponse(payload, status = 200) {
return new Response(JSON.stringify(payload), {
status,
headers: { "content-type": "application/json" },
});
}
test("UI-0 route is direct-only and does not add Polygon to system navigation", () => {
assert.deepEqual(resolvePolygonRunRoute("?workspace=polygon-run"), {
active: true,
runId: null,
error: null,
});
assert.deepEqual(
resolvePolygonRunRoute("?workspace=polygon-run&run=s1b-6cb1495-20260724t1535z"),
{
active: true,
runId: "s1b-6cb1495-20260724t1535z",
error: null,
},
);
assert.equal(resolvePolygonRunRoute("?workspace=missions").active, false);
assert.match(
resolvePolygonRunRoute("?workspace=polygon-run&run=../../etc").error,
/некорректный/,
);
assert.equal(workspaceById("polygon-run-internal").internalOnly, true);
assert.equal(
workspacesForRoot("system").some(({ id }) => id === "polygon-run-internal"),
false,
);
});
test("Polygon contracts decode path-free evidence and preserve the safety boundary", () => {
const decodedCatalog = decodePolygonRunCatalog(catalog());
const decodedDetail = decodePolygonRunDetail(detail());
assert.equal(decodedCatalog.items[0].state, "completed");
assert.equal(decodedDetail.run.authority.actuatorAuthority, false);
assert.equal(decodedDetail.run.authority.navigationOrSafetyAccepted, false);
assert.equal(decodedDetail.commandCount, 0);
assert.equal(decodedDetail.artifacts[0].relativePath, "provider-runtime/processes.json");
assert.equal("content" in decodedDetail, false);
});
test("Polygon contracts fail closed on command content, paths and unknown fields", () => {
assert.throws(
() => decodePolygonRunDetail(detail({
commands: { count: 1, content_exposed: true },
})),
PolygonRunContractError,
);
assert.throws(
() => decodePolygonRunDetail(detail({
artifacts: [{
...detail().artifacts[0],
relative_path: "/mnt/d/private/processes.json",
}],
})),
PolygonRunContractError,
);
assert.throws(
() => decodePolygonRunCatalog(catalog({ unexpected: true })),
PolygonRunContractError,
);
});
test("Polygon fetchers use bounded same-origin GET endpoints", async () => {
const calls = [];
const fetcher = async (url, init) => {
calls.push({ url, init });
return String(url).includes("event_limit") ? jsonResponse(detail()) : jsonResponse(catalog());
};
await fetchPolygonRunCatalog({ limit: 20, fetcher });
await fetchPolygonRunDetail("s1b-6cb1495-20260724t1535z", {
eventLimit: 200,
fetcher,
});
assert.deepEqual(calls.map(({ url }) => url), [
"/api/v1/polygon/runs?limit=20",
"/api/v1/polygon/runs/s1b-6cb1495-20260724t1535z?event_limit=200",
]);
assert.ok(calls.every(({ init }) => init.method === "GET"));
assert.ok(calls.every(({ init }) => init.headers.Accept === "application/json"));
});