feat(lab): stabilize autonomous TGS playback

This commit is contained in:
DCCONSTRUCTIONS
2026-08-27 20:34:20 +03:00
parent c8593207d3
commit 95a1ef5057
26 changed files with 3326 additions and 253 deletions
@@ -352,6 +352,7 @@ interface RecordedSegmentStreamRuntime {
target: RecordedSegmentTarget | null;
fetchAbort: AbortController | null;
notifiedRevision: number;
hasPresentedFrame: boolean;
pumping: boolean;
disposed: boolean;
onTargetBuffered: ((target: RecordedSegmentTarget) => void) | null;
@@ -944,6 +945,7 @@ export function RecordedFmp4Player({
target: null,
fetchAbort: null,
notifiedRevision: 0,
hasPresentedFrame: false,
pumping: false,
disposed: false,
onTargetBuffered: null,
@@ -1090,8 +1092,13 @@ export function RecordedFmp4Player({
runtime.fetchAbort?.abort();
runtime.target = target;
const alreadyBuffered = recordedSegmentTargetBuffered(runtime, target);
const retainForwardFrame = Boolean(
runtime.hasPresentedFrame
&& previousTarget
&& candidateTarget.sequence >= previousTarget.sequence,
);
video.pause();
if (!alreadyBuffered) {
if (!alreadyBuffered && !retainForwardFrame) {
setReadyGeneration(null);
setState("loading");
}
@@ -1119,6 +1126,7 @@ export function RecordedFmp4Player({
|| runtime.target?.revision !== bufferedTarget.revision
) return;
setBufferRevision((revision) => revision + 1);
runtime.hasPresentedFrame = true;
setReadyGeneration(runtime.generation);
setState("ready");
reportAdmission({
@@ -1,6 +1,7 @@
import {
forwardRef,
type CSSProperties,
useCallback,
useEffect,
useImperativeHandle,
useRef,
@@ -53,6 +54,12 @@ export interface LaboratoryMetricCellEvidence {
state: LaboratoryMetricCellState;
}
export interface LaboratoryMetricPackedCellEvidence {
centersBodyXyM: Float32Array;
zBoundsM: Float32Array;
stateCodes: Uint8Array;
}
export interface LaboratoryMetricLegendEntry {
id: LaboratoryMetricDecision
| LaboratoryMetricCellState
@@ -63,6 +70,13 @@ export interface LaboratoryMetricLegendEntry {
label: string;
}
interface LaboratoryMetricRenderStats {
frameMs: number;
drawCalls: number;
triangles: number;
pixelRatio: number;
}
export function laboratoryMetricLegendEntries({
pointCloudCount,
localSurfaceCount,
@@ -256,6 +270,7 @@ LaboratoryMetricEvidenceSceneHandle,
semanticClasses?: readonly RecordedEvidenceSemanticClass[];
semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[];
classifiedCells?: readonly LaboratoryMetricCellEvidence[];
classifiedPackedCells?: LaboratoryMetricPackedCellEvidence;
classifiedCellSizeM?: number;
showClassifiedCells?: boolean;
}
@@ -276,6 +291,7 @@ LaboratoryMetricEvidenceSceneHandle,
semanticClasses,
semanticPalette,
classifiedCells = [],
classifiedPackedCells,
classifiedCellSizeM = 0.45,
showClassifiedCells = true,
}, ref) {
@@ -288,10 +304,16 @@ LaboratoryMetricEvidenceSceneHandle,
const classifiedContentRef = useRef<THREE.Group | null>(null);
const localSurfacePointsRef = useRef<THREE.Points | null>(null);
const currentIncrementPointsRef = useRef<THREE.Points | null>(null);
const requestRenderRef = useRef<() => void>(() => undefined);
const corridorForwardLengthRef = useRef(corridor.forwardLengthM);
const modeRef = useRef(mode);
const classifiedMeshesRef = useRef<ReadonlyMap<LaboratoryMetricCellState, THREE.InstancedMesh>>(
new Map(),
);
const [renderError, setRenderError] = useState<string | null>(null);
const [renderStats, setRenderStats] = useState<LaboratoryMetricRenderStats | null>(null);
corridorForwardLengthRef.current = corridor.forwardLengthM;
modeRef.current = mode;
useEffect(() => {
const host = hostRef.current;
@@ -368,27 +390,51 @@ LaboratoryMetricEvidenceSceneHandle,
localSurfacePointsRef.current = localSurfacePoints;
currentIncrementPointsRef.current = currentIncrementPoints;
let animationFrame: number | null = null;
let lastStatsAt = Number.NEGATIVE_INFINITY;
const render = () => {
animationFrame = null;
// OrbitControls emits another change while damping is still settling, so
// this remains smooth during interaction without rendering forever while
// the evidence scene is idle.
controls.update();
const startedAt = performance.now();
renderer.render(scene, camera);
const completedAt = performance.now();
if (completedAt - lastStatsAt >= 1_000) {
lastStatsAt = completedAt;
setRenderStats({
frameMs: completedAt - startedAt,
drawCalls: renderer.info.render.calls,
triangles: renderer.info.render.triangles,
pixelRatio: renderer.getPixelRatio(),
});
}
};
const requestRender = () => {
if (animationFrame !== null) return;
animationFrame = window.requestAnimationFrame(render);
};
requestRenderRef.current = requestRender;
controls.addEventListener("change", requestRender);
const resize = () => {
const width = Math.max(host.clientWidth, 1);
const height = Math.max(host.clientHeight, 1);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height, false);
requestRender();
};
const observer = new ResizeObserver(resize);
observer.observe(host);
resize();
let animationFrame = 0;
const render = () => {
animationFrame = window.requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
};
render();
requestRender();
return () => {
window.cancelAnimationFrame(animationFrame);
if (animationFrame !== null) window.cancelAnimationFrame(animationFrame);
observer.disconnect();
controls.removeEventListener("change", requestRender);
controls.dispose();
scene.traverse(disposeRenderable);
renderer.dispose();
@@ -401,6 +447,7 @@ LaboratoryMetricEvidenceSceneHandle,
classifiedContentRef.current = null;
localSurfacePointsRef.current = null;
currentIncrementPointsRef.current = null;
requestRenderRef.current = () => undefined;
classifiedMeshesRef.current = new Map();
};
}, []);
@@ -415,6 +462,7 @@ LaboratoryMetricEvidenceSceneHandle,
if (!points) return;
points.visible = showLocalSurface && localSurfaceBodyXyzM.length > 0;
if (points.visible) updatePointPositions(points.geometry, localSurfaceBodyXyzM);
requestRenderRef.current();
}, [localSurfaceBodyXyzM, showLocalSurface]);
useEffect(() => {
@@ -422,7 +470,10 @@ LaboratoryMetricEvidenceSceneHandle,
const points = currentIncrementPointsRef.current;
if (!host || !points) return;
points.visible = showCurrentIncrement && pointCloudBodyXyzM.length > 0;
if (!points.visible) return;
if (!points.visible) {
requestRenderRef.current();
return;
}
updatePointPositions(points.geometry, pointCloudBodyXyzM);
const material = points.material as THREE.PointsMaterial;
const hasAlignedSemanticClasses =
@@ -436,6 +487,7 @@ LaboratoryMetricEvidenceSceneHandle,
material.needsUpdate = true;
}
material.color.copy(tokenColor(host, "--nodedc-text-muted", [147, 151, 159]));
requestRenderRef.current();
return;
}
const declaredIds = new Set(semanticClasses.map((item) => item.id));
@@ -461,6 +513,7 @@ LaboratoryMetricEvidenceSceneHandle,
material.needsUpdate = true;
}
material.color.setRGB(1, 1, 1);
requestRenderRef.current();
}, [
pointCloudBodyXyzM,
pointSemanticClassIds,
@@ -543,7 +596,7 @@ LaboratoryMetricEvidenceSceneHandle,
content.add(centroid);
}
}
requestRenderRef.current();
}, [
obstacles,
occupiedVoxelSizeM,
@@ -556,10 +609,21 @@ LaboratoryMetricEvidenceSceneHandle,
const host = hostRef.current;
const content = classifiedContentRef.current;
if (!host || !content) return;
content.visible = showClassifiedCells && classifiedCells.length > 0;
if (!content.visible) return;
const packedCellCount = classifiedPackedCells?.stateCodes.length ?? 0;
const packedCellsValid = !classifiedPackedCells || (
classifiedPackedCells.centersBodyXyM.length === packedCellCount * 2
&& classifiedPackedCells.zBoundsM.length === packedCellCount * 2
);
const cellCount = packedCellsValid && classifiedPackedCells
? packedCellCount
: classifiedCells.length;
content.visible = showClassifiedCells && cellCount > 0;
if (!content.visible) {
requestRenderRef.current();
return;
}
const requiredCapacity = classifiedCells.length;
const requiredCapacity = cellCount;
const currentMeshes = classifiedMeshesRef.current;
const firstMesh = currentMeshes.values().next().value as THREE.InstancedMesh | undefined;
const needsAllocation = !firstMesh
@@ -609,12 +673,16 @@ LaboratoryMetricEvidenceSceneHandle,
const position = new THREE.Vector3();
const scale = new THREE.Vector3(1, 1, 1);
const rotation = new THREE.Quaternion();
for (const cell of classifiedCells) {
const mesh = meshes.get(cell.state);
if (!mesh) continue;
const index = counts.get(cell.state) ?? 0;
const minimum = cell.zBoundsM[0];
const maximum = cell.zBoundsM[1];
const setCell = (
state: LaboratoryMetricCellState,
centerX: number,
centerY: number,
minimum: number | null,
maximum: number | null,
) => {
const mesh = meshes.get(state);
if (!mesh) return;
const index = counts.get(state) ?? 0;
const height = minimum === null || maximum === null
? 0.018
: Math.max(0.018, maximum - minimum);
@@ -622,21 +690,55 @@ LaboratoryMetricEvidenceSceneHandle,
? -0.012
: (minimum + maximum) / 2;
const [sceneX, sceneY, sceneZ] = scenePoint([
cell.centerBodyXyM[0],
cell.centerBodyXyM[1],
centerX,
centerY,
centerZ,
]);
position.set(sceneX, sceneY, sceneZ);
scale.set(1, height, 1);
matrix.compose(position, rotation, scale);
mesh.setMatrixAt(index, matrix);
counts.set(cell.state, index + 1);
counts.set(state, index + 1);
};
if (packedCellsValid && classifiedPackedCells) {
for (let cellIndex = 0; cellIndex < packedCellCount; cellIndex += 1) {
const code = classifiedPackedCells.stateCodes[cellIndex];
const state: LaboratoryMetricCellState = code === 1
? "ground-support"
: code === 2
? "nonground-occupied"
: code === 3
? "unknown-rejected"
: code === 0
? "unobserved"
: "unknown-rejected";
const minimum = classifiedPackedCells.zBoundsM[cellIndex * 2]!;
const maximum = classifiedPackedCells.zBoundsM[cellIndex * 2 + 1]!;
setCell(
state,
classifiedPackedCells.centersBodyXyM[cellIndex * 2]!,
classifiedPackedCells.centersBodyXyM[cellIndex * 2 + 1]!,
Number.isFinite(minimum) ? minimum : null,
Number.isFinite(maximum) ? maximum : null,
);
}
} else {
for (const cell of classifiedCells) {
setCell(
cell.state,
cell.centerBodyXyM[0],
cell.centerBodyXyM[1],
cell.zBoundsM[0],
cell.zBoundsM[1],
);
}
}
for (const [state, mesh] of meshes) {
mesh.count = counts.get(state) ?? 0;
mesh.instanceMatrix.needsUpdate = true;
}
}, [classifiedCellSizeM, classifiedCells, showClassifiedCells]);
requestRenderRef.current();
}, [classifiedCellSizeM, classifiedCells, classifiedPackedCells, showClassifiedCells]);
useEffect(() => {
const host = hostRef.current;
@@ -702,15 +804,24 @@ LaboratoryMetricEvidenceSceneHandle,
material.depthWrite = false;
});
content.add(grid);
}, [corridor, rig]);
requestRenderRef.current();
}, [
corridor.forwardLengthM,
corridor.halfWidthM,
corridor.rearMarginM,
rig.lengthM,
rig.nominalSensorHeightM,
rig.widthM,
]);
const resetView = () => {
const resetView = useCallback(() => {
const camera = cameraRef.current;
const controls = controlsRef.current;
if (!camera || !controls) return;
controls.target.set(corridor.forwardLengthM * 0.35, 0.6, 0);
if (mode === "plan") {
camera.position.set(corridor.forwardLengthM * 0.35, 15, 0.001);
const forwardLengthM = corridorForwardLengthRef.current;
controls.target.set(forwardLengthM * 0.35, 0.6, 0);
if (modeRef.current === "plan") {
camera.position.set(forwardLengthM * 0.35, 15, 0.001);
camera.up.set(0, 0, -1);
} else {
camera.position.set(-4.5, 4.8, 8.5);
@@ -718,10 +829,13 @@ LaboratoryMetricEvidenceSceneHandle,
}
camera.updateProjectionMatrix();
controls.update();
};
requestRenderRef.current();
}, []);
useEffect(resetView, [corridor.forwardLengthM, mode]);
useImperativeHandle(ref, () => ({ resetView }));
// Playback data and rig object identities must never reset an operator's
// orbit. Only a deliberate 3D/PLAN mode transition chooses a default view.
useEffect(() => resetView(), [mode, resetView]);
useImperativeHandle(ref, () => ({ resetView }), [resetView]);
const semanticLegendEntries = (() => {
if (
@@ -755,8 +869,23 @@ LaboratoryMetricEvidenceSceneHandle,
showLowStep,
});
const classifiedLegendEntries = (() => {
if (!showClassifiedCells || !classifiedCells.length) return [];
const states = new Set(classifiedCells.map((cell) => cell.state));
const states = new Set<LaboratoryMetricCellState>();
if (classifiedPackedCells) {
for (const code of classifiedPackedCells.stateCodes) {
states.add(code === 1
? "ground-support"
: code === 2
? "nonground-occupied"
: code === 3
? "unknown-rejected"
: code === 0
? "unobserved"
: "unknown-rejected");
}
} else {
classifiedCells.forEach((cell) => states.add(cell.state));
}
if (!showClassifiedCells || !states.size) return [];
return [
states.has("ground-support")
? { id: "ground-support" as const, label: "Ground support" }
@@ -783,6 +912,16 @@ LaboratoryMetricEvidenceSceneHandle,
{renderError ? <p>{renderError}</p> : null}
</div>
<div className="laboratory-metric-evidence-scene__legend">
{renderStats ? (
<span
data-decision="performance"
title="CPU-время отправки последнего WebGL render pass; это не GPU timer. В покое сцена не перерисовывается."
>
3D {renderStats.frameMs.toFixed(1)} ms · {renderStats.drawCalls} draw · {renderStats.triangles.toLocaleString("ru-RU")} tri · {(
classifiedPackedCells?.stateCodes.length ?? classifiedCells.length
).toLocaleString("ru-RU")} cells · {renderStats.pixelRatio.toFixed(1)}×
</span>
) : null}
{metricLegendEntries.map((entry) => (
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
))}
@@ -0,0 +1,671 @@
import type { LaboratoryFetch } from "./advancedResults";
const RESULT_ID = /^m49-physical-safety-playback-[a-f0-9]{64}$/;
const SOURCE_RESULT_ID = /^m49-tgs-full-shadow-[a-f0-9]{64}$/;
const SCHEMA = "missioncore.m49-physical-safety-playback/v1";
const CHUNK_MAGIC = "MCPSCH01";
const CHUNK_HEADER_BYTES = 28;
const ENDPOINT_ROOT = "/api/v1/laboratory/m49/physical-safety-playback";
export interface M49PhysicalSafetyPlaybackFrameMetadata {
sourceSequence: number;
sourceFrameIndex: number;
sessionSeconds: number;
sampleAvailable: boolean;
metrics: Readonly<Record<string, number>>;
}
interface ArtifactDescriptor {
url: string;
mediaType: string;
byteLength: number;
sha256: string;
}
interface ChunkDescriptor extends ArtifactDescriptor {
index: number;
start: number;
count: number;
headerBytes: 28;
format: "mcpsch01-states-u8-aligned-z-bounds-f32le";
}
export interface M49PhysicalSafetyPlaybackManifest {
resultId: string;
createdAtUtc: string;
sourceResultId: string;
frameCount: number;
cellCount: number;
cellSizeM: number;
radiusM: number;
sourcePaceHz: number;
stateCodes: Readonly<Record<string, number>>;
chunkFrameCount: 32;
startupPrebufferChunkCount: 2;
residentChunkCountMax: 3;
forwardPrefetchChunkCount: 1;
centers: ArtifactDescriptor;
frames: ArtifactDescriptor;
chunks: readonly ChunkDescriptor[];
}
interface ResidentChunk {
descriptor: ChunkDescriptor;
states: Uint8Array;
zBoundsM: Float32Array;
byteLength: number;
}
export interface M49PhysicalSafetyPlaybackFrame {
metadata: M49PhysicalSafetyPlaybackFrameMetadata;
centersXyM: Float32Array;
states: Uint8Array;
zBoundsM: Float32Array;
cellSizeM: number;
radiusM: number;
}
export interface M49PhysicalSafetyPlaybackProgress {
phase: "manifest" | "static" | "prebuffer" | "ready" | "seek";
residentChunkIndexes: readonly number[];
loadedBytes: number;
}
export class M49PhysicalSafetyPlaybackContractError extends Error {}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new M49PhysicalSafetyPlaybackContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function text(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new M49PhysicalSafetyPlaybackContractError(`${label}: ожидалась строка.`);
}
return value;
}
function finiteNumber(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new M49PhysicalSafetyPlaybackContractError(`${label}: ожидалось число.`);
}
return value;
}
function integer(value: unknown, label: string): number {
const parsed = finiteNumber(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new M49PhysicalSafetyPlaybackContractError(`${label}: ожидалось целое число.`);
}
return parsed;
}
function booleanValue(value: unknown, label: string): boolean {
if (typeof value !== "boolean") {
throw new M49PhysicalSafetyPlaybackContractError(`${label}: ожидался boolean.`);
}
return value;
}
function exact<T extends string | number | boolean>(value: unknown, expected: T, label: string): T {
if (value !== expected) {
throw new M49PhysicalSafetyPlaybackContractError(`${label}: нарушен контракт.`);
}
return expected;
}
function sha256(value: unknown, label: string): string {
const parsed = text(value, label);
if (!/^[a-f0-9]{64}$/.test(parsed)) {
throw new M49PhysicalSafetyPlaybackContractError(`${label}: SHA-256 недопустим.`);
}
return parsed;
}
function localUrl(value: unknown, expected: string, label: string): string {
const parsed = text(value, label);
if (parsed !== expected || parsed.includes("worker")) {
throw new M49PhysicalSafetyPlaybackContractError(`${label}: разрешён только sealed local API.`);
}
return parsed;
}
function descriptor(
value: unknown,
expectedUrl: string,
label: string,
): ArtifactDescriptor {
const row = objectValue(value, label);
return {
url: localUrl(row.url, expectedUrl, `${label}.url`),
mediaType: text(row.media_type, `${label}.media_type`),
byteLength: integer(row.byte_length, `${label}.byte_length`),
sha256: sha256(row.sha256, `${label}.sha256`),
};
}
async function sha256Hex(buffer: ArrayBuffer): Promise<string> {
if (!globalThis.crypto?.subtle) {
throw new M49PhysicalSafetyPlaybackContractError(
"Браузер не поддерживает проверку playback SHA-256.",
);
}
const digest = await globalThis.crypto.subtle.digest("SHA-256", buffer);
return [...new Uint8Array(digest)]
.map((value) => value.toString(16).padStart(2, "0"))
.join("");
}
async function fetchVerified(
artifact: ArtifactDescriptor,
fetcher: LaboratoryFetch,
signal?: AbortSignal,
): Promise<ArrayBuffer> {
const response = await fetcher(artifact.url, {
method: "GET",
headers: { Accept: artifact.mediaType },
signal,
});
if (!response.ok) {
throw new M49PhysicalSafetyPlaybackContractError(
`Sealed local playback недоступен: HTTP ${response.status}.`,
);
}
const buffer = await response.arrayBuffer();
if (buffer.byteLength !== artifact.byteLength) {
throw new M49PhysicalSafetyPlaybackContractError(
"Sealed local playback: получен неверный размер.",
);
}
if (await sha256Hex(buffer) !== artifact.sha256) {
throw new M49PhysicalSafetyPlaybackContractError(
"Sealed local playback: SHA-256 не совпал.",
);
}
return buffer;
}
function parseStateCodes(value: unknown): Readonly<Record<string, number>> {
const row = objectValue(value, "M49 physical-safety state codes");
const parsed: Record<string, number> = {};
for (const [name, code] of Object.entries(row)) {
const stateCode = integer(code, `M49 physical-safety state code ${name}`);
if (stateCode > 255) {
throw new M49PhysicalSafetyPlaybackContractError("M49 state code превышает uint8.");
}
parsed[name] = stateCode;
}
if (parsed.UNOBSERVED !== 0 || !Object.keys(parsed).length) {
throw new M49PhysicalSafetyPlaybackContractError("M49 UNOBSERVED state изменился.");
}
return parsed;
}
async function fetchManifest(
resultId: string,
fetcher: LaboratoryFetch,
signal?: AbortSignal,
): Promise<M49PhysicalSafetyPlaybackManifest> {
if (!RESULT_ID.test(resultId)) {
throw new M49PhysicalSafetyPlaybackContractError("M49 physical-safety identity недопустима.");
}
const base = `${ENDPOINT_ROOT}/${encodeURIComponent(resultId)}`;
const response = await fetcher(`${base}/manifest`, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new M49PhysicalSafetyPlaybackContractError(
`M49 physical-safety manifest недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "M49 physical-safety manifest");
exact(payload.schema_version, SCHEMA, "M49 physical-safety schema");
exact(payload.result_id, resultId, "M49 physical-safety result");
exact(payload.access, "read-only-sealed-local", "M49 physical-safety access");
const identity = objectValue(payload.identity, "M49 physical-safety identity");
const execution = objectValue(payload.execution, "M49 physical-safety execution");
exact(execution.execution_class, "local-sequential-offline", "M49 execution class");
exact(execution.worker_role, "realtime-only", "M49 worker role");
exact(execution.worker_runtime_dependency, false, "M49 worker dependency");
exact(execution.worker_requests_required, 0, "M49 worker requests");
const authority = objectValue(payload.authority, "M49 physical-safety authority");
exact(authority.commands_enabled, false, "M49 commands");
exact(authority.navigation_or_safety_accepted, false, "M49 safety authority");
exact(authority.actuation_accepted, false, "M49 actuation authority");
const playback = objectValue(payload.playback, "M49 physical-safety playback");
exact(playback.coordinate_frame, "map-gravity-local", "M49 coordinate frame");
const frameCount = integer(playback.frame_count, "M49 frame count");
const cellCount = integer(playback.cell_count, "M49 cell count");
if (!frameCount || !cellCount) {
throw new M49PhysicalSafetyPlaybackContractError("M49 playback не содержит evidence.");
}
const chunkFrameCount = exact(
integer(playback.chunk_frame_count, "M49 chunk frame count"),
32,
"M49 chunk frame count",
);
const startupPrebufferChunkCount = exact(
integer(playback.startup_prebuffer_chunk_count, "M49 startup prebuffer"),
2,
"M49 startup prebuffer",
);
const residentChunkCountMax = exact(
integer(playback.resident_chunk_count_max, "M49 resident chunks"),
3,
"M49 resident chunks",
);
const forwardPrefetchChunkCount = exact(
integer(playback.forward_prefetch_chunk_count, "M49 forward prefetch"),
1,
"M49 forward prefetch",
);
const centers = descriptor(playback.centers, `${base}/tracks/centers`, "M49 centers");
const centerRow = objectValue(playback.centers, "M49 centers");
exact(centerRow.dtype, "<f4", "M49 centers dtype");
if (!Array.isArray(centerRow.shape)
|| centerRow.shape.length !== 2
|| centerRow.shape[0] !== cellCount
|| centerRow.shape[1] !== 2
|| centers.byteLength !== cellCount * 2 * 4) {
throw new M49PhysicalSafetyPlaybackContractError("M49 centers shape изменилась.");
}
const frames = descriptor(playback.frames, `${base}/tracks/frames`, "M49 frames");
const frameRow = objectValue(playback.frames, "M49 frames");
exact(frameRow.dtype, "ndjson", "M49 frames dtype");
if (!Array.isArray(frameRow.shape)
|| frameRow.shape.length !== 1
|| frameRow.shape[0] !== frameCount) {
throw new M49PhysicalSafetyPlaybackContractError("M49 frames shape изменилась.");
}
if (!Array.isArray(playback.chunks)) {
throw new M49PhysicalSafetyPlaybackContractError("M49 chunk catalog отсутствует.");
}
const expectedChunkCount = Math.ceil(frameCount / chunkFrameCount);
if (playback.chunks.length !== expectedChunkCount) {
throw new M49PhysicalSafetyPlaybackContractError("M49 chunk accounting нарушен.");
}
const chunks = playback.chunks.map((value, index): ChunkDescriptor => {
const row = objectValue(value, `M49 chunk ${index}`);
const common = descriptor(value, `${base}/chunks/${index}`, `M49 chunk ${index}`);
const start = index * chunkFrameCount;
const count = Math.min(chunkFrameCount, frameCount - start);
exact(integer(row.index, `M49 chunk ${index}.index`), index, `M49 chunk ${index}.index`);
exact(integer(row.start, `M49 chunk ${index}.start`), start, `M49 chunk ${index}.start`);
exact(integer(row.count, `M49 chunk ${index}.count`), count, `M49 chunk ${index}.count`);
return {
...common,
index,
start,
count,
headerBytes: exact(
integer(row.header_bytes, `M49 chunk ${index}.header`),
CHUNK_HEADER_BYTES,
`M49 chunk ${index}.header`,
),
format: exact(
row.format,
"mcpsch01-states-u8-aligned-z-bounds-f32le",
`M49 chunk ${index}.format`,
),
};
});
return {
resultId,
createdAtUtc: text(payload.created_at_utc, "M49 physical-safety created"),
sourceResultId: text(identity.source_result_id, "M49 physical-safety source"),
frameCount,
cellCount,
cellSizeM: finiteNumber(playback.cell_size_m, "M49 cell size"),
radiusM: finiteNumber(playback.radius_m, "M49 radius"),
sourcePaceHz: finiteNumber(playback.source_pace_hz, "M49 source pace"),
stateCodes: parseStateCodes(playback.state_codes),
chunkFrameCount,
startupPrebufferChunkCount,
residentChunkCountMax,
forwardPrefetchChunkCount,
centers,
frames,
chunks,
};
}
export async function fetchM49PhysicalSafetyPlaybackIdForSource(
sourceResultId: string,
{
fetcher = fetch,
signal,
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<string | null> {
if (!SOURCE_RESULT_ID.test(sourceResultId)) {
throw new M49PhysicalSafetyPlaybackContractError("M49 source identity недопустима.");
}
const response = await fetcher(
`${ENDPOINT_ROOT}/results?limit=2&source_result_id=${encodeURIComponent(sourceResultId)}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new M49PhysicalSafetyPlaybackContractError(
`M49 physical-safety catalog недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "M49 physical-safety catalog");
exact(
payload.schema_version,
"missioncore.m49-physical-safety-playback-catalog/v1",
"M49 physical-safety catalog schema",
);
exact(payload.worker_runtime_dependency, false, "M49 physical-safety catalog worker");
exact(payload.access, "read-only-sealed-local", "M49 physical-safety catalog access");
if (!Array.isArray(payload.items)) {
throw new M49PhysicalSafetyPlaybackContractError("M49 physical-safety catalog изменился.");
}
if (!payload.items.length) return null;
if (payload.items.length !== 1) {
throw new M49PhysicalSafetyPlaybackContractError(
"M49 physical-safety source имеет несколько активных playback artifacts.",
);
}
const item = objectValue(payload.items[0], "M49 physical-safety catalog item");
exact(item.source_result_id, sourceResultId, "M49 physical-safety catalog source");
exact(item.worker_runtime_dependency, false, "M49 physical-safety item worker");
exact(item.navigation_or_safety_accepted, false, "M49 physical-safety item authority");
const resultId = text(item.result_id, "M49 physical-safety catalog result");
if (!RESULT_ID.test(resultId)) {
throw new M49PhysicalSafetyPlaybackContractError(
"M49 physical-safety catalog result недопустим.",
);
}
return resultId;
}
function parseFrames(
buffer: ArrayBuffer,
frameCount: number,
): readonly M49PhysicalSafetyPlaybackFrameMetadata[] {
const lines = new TextDecoder().decode(buffer).trim().split("\n");
if (lines.length !== frameCount) {
throw new M49PhysicalSafetyPlaybackContractError("M49 frame catalog изменил размер.");
}
return lines.map((line, sourceSequence) => {
const row = objectValue(JSON.parse(line), `M49 frame ${sourceSequence}`);
const metrics: Record<string, number> = {};
for (const [name, value] of Object.entries(row)) {
if (name.endsWith("_count") && typeof value === "number" && Number.isFinite(value)) {
metrics[name] = value;
}
}
return {
sourceSequence,
sourceFrameIndex: integer(row.source_frame_index, `M49 frame ${sourceSequence}.source`),
sessionSeconds: finiteNumber(row.session_seconds, `M49 frame ${sourceSequence}.time`),
sampleAvailable: booleanValue(
row.sample_available,
`M49 frame ${sourceSequence}.available`,
),
metrics,
};
});
}
function parseChunk(
buffer: ArrayBuffer,
descriptorValue: ChunkDescriptor,
cellCount: number,
frames: readonly M49PhysicalSafetyPlaybackFrameMetadata[],
admittedStateCodes: ReadonlySet<number>,
): ResidentChunk {
const bytes = new Uint8Array(buffer);
if (bytes.byteLength < CHUNK_HEADER_BYTES
|| new TextDecoder("latin1").decode(bytes.subarray(0, 8)) !== CHUNK_MAGIC) {
throw new M49PhysicalSafetyPlaybackContractError("M49 chunk signature изменилась.");
}
const view = new DataView(buffer);
const start = view.getUint32(8, true);
const count = view.getUint32(12, true);
const cells = view.getUint32(16, true);
const stateBytes = view.getUint32(20, true);
const zBytes = view.getUint32(24, true);
const expectedStateBytes = count * cellCount;
const expectedZBytes = count * cellCount * 2 * 4;
const zOffset = CHUNK_HEADER_BYTES + stateBytes
+ ((4 - ((CHUNK_HEADER_BYTES + stateBytes) % 4)) % 4);
if (start !== descriptorValue.start
|| count !== descriptorValue.count
|| cells !== cellCount
|| stateBytes !== expectedStateBytes
|| zBytes !== expectedZBytes
|| zOffset + zBytes !== bytes.byteLength) {
throw new M49PhysicalSafetyPlaybackContractError("M49 chunk layout изменился.");
}
const states = new Uint8Array(buffer, CHUNK_HEADER_BYTES, stateBytes);
const zBoundsM = new Float32Array(buffer, zOffset, count * cellCount * 2);
for (let localFrame = 0; localFrame < count; localFrame += 1) {
const stateStart = localFrame * cellCount;
const zStart = localFrame * cellCount * 2;
for (let cell = 0; cell < cellCount; cell += 1) {
const state = states[stateStart + cell];
if (!admittedStateCodes.has(state)) {
throw new M49PhysicalSafetyPlaybackContractError("M49 chunk содержит неизвестный state.");
}
if (!frames[start + localFrame].sampleAvailable
&& (state !== 0
|| Number.isFinite(zBoundsM[zStart + cell * 2])
|| Number.isFinite(zBoundsM[zStart + cell * 2 + 1]))) {
throw new M49PhysicalSafetyPlaybackContractError(
"M49 missing sample перестал быть fail-closed.",
);
}
}
}
return { descriptor: descriptorValue, states, zBoundsM, byteLength: buffer.byteLength };
}
export class M49PhysicalSafetyPlaybackBuffer {
readonly manifest: M49PhysicalSafetyPlaybackManifest;
readonly centersXyM: Float32Array;
readonly frames: readonly M49PhysicalSafetyPlaybackFrameMetadata[];
private readonly fetcher: LaboratoryFetch;
private readonly signal: AbortSignal | undefined;
private readonly onProgress: ((value: M49PhysicalSafetyPlaybackProgress) => void) | undefined;
private readonly chunks = new Map<number, ResidentChunk>();
private readonly pending = new Map<number, Promise<ResidentChunk>>();
private readonly admittedStateCodes: ReadonlySet<number>;
private staticBytes: number;
private preparedWindowKey: string | null = null;
private pendingPrepare: { key: string; promise: Promise<void> } | null = null;
private constructor(
manifest: M49PhysicalSafetyPlaybackManifest,
centersXyM: Float32Array,
frames: readonly M49PhysicalSafetyPlaybackFrameMetadata[],
fetcher: LaboratoryFetch,
signal: AbortSignal | undefined,
onProgress: ((value: M49PhysicalSafetyPlaybackProgress) => void) | undefined,
) {
this.manifest = manifest;
this.centersXyM = centersXyM;
this.frames = frames;
this.fetcher = fetcher;
this.signal = signal;
this.onProgress = onProgress;
this.admittedStateCodes = new Set(Object.values(manifest.stateCodes));
this.staticBytes = centersXyM.byteLength + manifest.frames.byteLength;
}
static async open(
resultId: string,
{
fetcher = fetch,
signal,
onProgress,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
onProgress?: (value: M49PhysicalSafetyPlaybackProgress) => void;
} = {},
): Promise<M49PhysicalSafetyPlaybackBuffer> {
onProgress?.({ phase: "manifest", residentChunkIndexes: [], loadedBytes: 0 });
const manifest = await fetchManifest(resultId, fetcher, signal);
const [centersBuffer, framesBuffer] = await Promise.all([
fetchVerified(manifest.centers, fetcher, signal),
fetchVerified(manifest.frames, fetcher, signal),
]);
onProgress?.({
phase: "static",
residentChunkIndexes: [],
loadedBytes: centersBuffer.byteLength + framesBuffer.byteLength,
});
const centersXyM = new Float32Array(centersBuffer);
const frames = parseFrames(framesBuffer, manifest.frameCount);
const playback = new M49PhysicalSafetyPlaybackBuffer(
manifest,
centersXyM,
frames,
fetcher,
signal,
onProgress,
);
const startupCount = Math.min(
manifest.startupPrebufferChunkCount,
manifest.chunks.length,
);
await Promise.all(Array.from(
{ length: startupCount },
(_, index) => playback.loadChunk(index, "prebuffer"),
));
playback.emit("ready");
return playback;
}
get residentChunkIndexes(): readonly number[] {
return [...this.chunks.keys()].sort((left, right) => left - right);
}
get residentByteLength(): number {
return this.staticBytes
+ [...this.chunks.values()].reduce((sum, chunk) => sum + chunk.byteLength, 0);
}
async prepare(sourceSequence: number): Promise<void> {
const chunkIndex = this.chunkIndex(sourceSequence);
const nextIndex = chunkIndex + this.manifest.forwardPrefetchChunkCount;
const indexes = [chunkIndex, nextIndex].filter(
(index) => index < this.manifest.chunks.length,
);
const key = indexes.join(":");
if (this.preparedWindowKey === key && indexes.every((index) => this.chunks.has(index))) {
return;
}
if (this.pendingPrepare?.key === key) return this.pendingPrepare.promise;
const promise = Promise.all(indexes.map((index) => this.loadChunk(index, "seek")))
.then(() => {
this.trim(new Set(indexes));
this.preparedWindowKey = key;
this.emit("seek");
})
.finally(() => {
if (this.pendingPrepare?.key === key) this.pendingPrepare = null;
});
this.pendingPrepare = { key, promise };
return promise;
}
async frame(sourceSequence: number): Promise<M49PhysicalSafetyPlaybackFrame> {
const chunkIndex = this.chunkIndex(sourceSequence);
const chunk = await this.loadChunk(chunkIndex, "seek");
const frame = this.frameFromChunk(sourceSequence, chunk);
const nextIndex = chunkIndex + this.manifest.forwardPrefetchChunkCount;
if (nextIndex < this.manifest.chunks.length && !this.chunks.has(nextIndex)) {
void this.loadChunk(nextIndex, "seek").catch(() => undefined);
}
return frame;
}
frameIfResident(sourceSequence: number): M49PhysicalSafetyPlaybackFrame | null {
const chunkIndex = this.chunkIndex(sourceSequence);
const chunk = this.chunks.get(chunkIndex);
return chunk ? this.frameFromChunk(sourceSequence, chunk) : null;
}
private frameFromChunk(
sourceSequence: number,
chunk: ResidentChunk,
): M49PhysicalSafetyPlaybackFrame {
const localFrame = sourceSequence - chunk.descriptor.start;
const stateOffset = localFrame * this.manifest.cellCount;
const zOffset = stateOffset * 2;
return {
metadata: this.frames[sourceSequence],
centersXyM: this.centersXyM,
states: chunk.states.subarray(stateOffset, stateOffset + this.manifest.cellCount),
zBoundsM: chunk.zBoundsM.subarray(zOffset, zOffset + this.manifest.cellCount * 2),
cellSizeM: this.manifest.cellSizeM,
radiusM: this.manifest.radiusM,
};
}
private chunkIndex(sourceSequence: number): number {
if (!Number.isSafeInteger(sourceSequence)
|| sourceSequence < 0
|| sourceSequence >= this.manifest.frameCount) {
throw new M49PhysicalSafetyPlaybackContractError("M49 source sequence недопустима.");
}
return Math.floor(sourceSequence / this.manifest.chunkFrameCount);
}
private async loadChunk(
index: number,
phase: "prebuffer" | "seek",
): Promise<ResidentChunk> {
const existing = this.chunks.get(index);
if (existing) {
this.chunks.delete(index);
this.chunks.set(index, existing);
return existing;
}
const inflight = this.pending.get(index);
if (inflight) return inflight;
const descriptorValue = this.manifest.chunks[index];
if (!descriptorValue) {
throw new M49PhysicalSafetyPlaybackContractError("M49 chunk отсутствует.");
}
const promise = fetchVerified(descriptorValue, this.fetcher, this.signal)
.then((buffer) => parseChunk(
buffer,
descriptorValue,
this.manifest.cellCount,
this.frames,
this.admittedStateCodes,
))
.then((chunk) => {
this.chunks.set(index, chunk);
this.trim(new Set([index]));
this.emit(phase);
return chunk;
})
.finally(() => this.pending.delete(index));
this.pending.set(index, promise);
return promise;
}
private trim(protectedIndexes: ReadonlySet<number>): void {
while (this.chunks.size > this.manifest.residentChunkCountMax) {
const removable = [...this.chunks.keys()].find((index) => !protectedIndexes.has(index));
if (removable === undefined) break;
this.chunks.delete(removable);
}
}
private emit(phase: M49PhysicalSafetyPlaybackProgress["phase"]): void {
this.onProgress?.({
phase,
residentChunkIndexes: this.residentChunkIndexes,
loadedBytes: this.residentByteLength,
});
}
}
@@ -120,7 +120,7 @@ export interface M49TgsFullShadowPlaybackPack {
frameCount: 4489;
cellCount: 2244;
totalByteLength: number;
centersXyM: readonly (readonly [number, number])[];
centersXyM: Float32Array;
states: Uint8Array;
zBoundsM: Float32Array;
frames: readonly M49TgsFullShadowPlaybackFrame[];
@@ -628,9 +628,6 @@ export async function fetchM49TgsFullShadowPlaybackPack(
"<f4",
centersTrack.shape,
) as Float32Array;
const centersXyM = Array.from({ length: manifest.cellCount }, (_, index) => (
[centersRaw[index * 2]!, centersRaw[index * 2 + 1]!] as const
));
const states = parseM49TgsNpyTrack(
buffers.get("states")!,
"|u1",
@@ -653,7 +650,7 @@ export async function fetchM49TgsFullShadowPlaybackPack(
frameCount: manifest.frameCount,
cellCount: manifest.cellCount,
totalByteLength: manifest.totalByteLength,
centersXyM,
centersXyM: centersRaw,
states,
zBoundsM,
frames,
@@ -238,6 +238,37 @@ export interface M4ThreatPlaybackPointPack {
pointCount: number;
pointOffsets: Uint32Array;
pointsMapXyzM: Float32Array;
pointStart?: number;
startSequence?: number;
sequenceCount?: number;
}
export interface M4ThreatPlaybackChunkDescriptor {
index: number;
start: number;
count: number;
pointStart: number;
pointCount: number;
url: string;
mediaType: "application/octet-stream";
byteLength: number;
sha256: string;
}
export interface M4ThreatPlaybackManifest {
resultId: string;
frameCount: 4489;
pointCount: number;
pointOffsets: Uint32Array;
chunkFrameCount: 24;
residentChunkCountMax: 4;
forwardPrefetchChunkCount: 1;
chunks: readonly M4ThreatPlaybackChunkDescriptor[];
track: {
url: string;
byteLength: number;
sha256: string;
};
}
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
@@ -889,7 +920,14 @@ export function hydrateM4ThreatTimelineFrame(
frame: M4ThreatTimelineFrame,
pack: M4ThreatPlaybackPointPack,
): M4ThreatTimelineFrame {
if (frame.sequence >= pack.frameCount || pack.resultId === "") {
if (
frame.sequence >= pack.frameCount
|| pack.resultId === ""
|| (pack.startSequence !== undefined && (
frame.sequence < pack.startSequence
|| frame.sequence >= pack.startSequence + (pack.sequenceCount ?? 0)
))
) {
throw new M4ThreatContractError("M4.6 binary playback frame: нарушена идентичность.");
}
if (!frame.sourceAvailable || !frame.spatialAvailable || !frame.bodyFrame) {
@@ -900,7 +938,15 @@ export function hydrateM4ThreatTimelineFrame(
}
const start = pack.pointOffsets[frame.sequence];
const stop = pack.pointOffsets[frame.sequence + 1];
if (start === undefined || stop === undefined || stop < start || stop > pack.pointCount) {
const pointStart = pack.pointStart ?? 0;
const pointStop = pointStart + pack.pointCount;
if (
start === undefined
|| stop === undefined
|| stop < start
|| start < pointStart
|| stop > pointStop
) {
throw new M4ThreatContractError("M4.6 binary playback offsets: нарушена размерность.");
}
const count = stop - start;
@@ -911,7 +957,7 @@ export function hydrateM4ThreatTimelineFrame(
const basis = frame.bodyFrame.basisMapFromBody;
const points = new Array<M4Point3>(count);
for (let index = 0; index < count; index += 1) {
const sourceOffset = (start + index) * 3;
const sourceOffset = (start - pointStart + index) * 3;
const dx = pack.pointsMapXyzM[sourceOffset]! - origin[0];
const dy = pack.pointsMapXyzM[sourceOffset + 1]! - origin[1];
const dz = pack.pointsMapXyzM[sourceOffset + 2]! - origin[2];
@@ -924,22 +970,19 @@ export function hydrateM4ThreatTimelineFrame(
return { ...frame, pointCloudBodyXyzM: points, pointCloudSampleCount: count };
}
export async function fetchM4ThreatPlaybackPointPack(
export async function fetchM4ThreatPlaybackManifest(
result: string,
{
fetcher = fetch,
signal,
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
onProgress,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
endpointRoot?: string;
onProgress?: (progress: M4ThreatPlaybackProgress) => void;
} = {},
): Promise<M4ThreatPlaybackPointPack> {
): Promise<M4ThreatPlaybackManifest> {
resultId(result);
onProgress?.({ phase: "manifest", loadedBytes: 0, totalBytes: 0 });
const manifestResponse = await fetcher(`${endpointRoot}/${result}/timeline/playback`, {
headers: { Accept: "application/json" },
signal,
@@ -982,46 +1025,180 @@ export async function fetchM4ThreatPlaybackPointPack(
if (!/^[a-f0-9]{64}$/.test(sha256)) {
throw new M4ThreatContractError("M4.6 playback SHA-256: нарушен контракт.");
}
const response = await fetcher(text(track.url, "M4.6 playback URL"), {
headers: { Accept: "application/octet-stream" },
signal,
const trackUrl = text(track.url, "M4.6 playback URL");
const chunkFrameCount = exact(
integer(manifest.chunk_frame_count, "M4.6 playback chunk frames"),
24,
"M4.6 playback chunk frames",
);
const residentChunkCountMax = exact(
integer(manifest.resident_chunk_count_max, "M4.6 playback resident chunks"),
4,
"M4.6 playback resident chunks",
);
const forwardPrefetchChunkCount = exact(
integer(manifest.forward_prefetch_chunk_count, "M4.6 playback prefetch chunks"),
1,
"M4.6 playback prefetch chunks",
);
const chunkRows = array(manifest.chunks, "M4.6 playback chunks");
const expectedChunkCount = Math.ceil(frameCount / chunkFrameCount);
if (chunkRows.length !== expectedChunkCount) {
throw new M4ThreatContractError("M4.6 playback chunk catalog: неверный размер.");
}
const chunks = chunkRows.map((value, index): M4ThreatPlaybackChunkDescriptor => {
const row = object(value, `M4.6 playback chunk ${index}`);
const start = index * chunkFrameCount;
const count = Math.min(chunkFrameCount, frameCount - start);
const pointStart = pointOffsets[start]!;
const pointStop = pointOffsets[start + count]!;
const pointChunkCount = pointStop - pointStart;
exact(integer(row.index, `M4.6 playback chunk ${index}.index`), index, `M4.6 playback chunk ${index}.index`);
exact(integer(row.start, `M4.6 playback chunk ${index}.start`), start, `M4.6 playback chunk ${index}.start`);
exact(integer(row.count, `M4.6 playback chunk ${index}.count`), count, `M4.6 playback chunk ${index}.count`);
exact(integer(row.point_start, `M4.6 playback chunk ${index}.point start`), pointStart, `M4.6 playback chunk ${index}.point start`);
exact(integer(row.point_count, `M4.6 playback chunk ${index}.point count`), pointChunkCount, `M4.6 playback chunk ${index}.point count`);
exact(row.media_type, "application/octet-stream", `M4.6 playback chunk ${index}.media type`);
exact(row.dtype, "<f4", `M4.6 playback chunk ${index}.dtype`);
const shapeValue = array(row.shape, `M4.6 playback chunk ${index}.shape`)
.map((item) => integer(item, `M4.6 playback chunk ${index}.shape`));
const chunkBytes = integer(row.bytes, `M4.6 playback chunk ${index}.bytes`);
if (
shapeValue.length !== 2
|| shapeValue[0] !== pointChunkCount
|| shapeValue[1] !== 3
|| chunkBytes !== pointChunkCount * 3 * Float32Array.BYTES_PER_ELEMENT
) {
throw new M4ThreatContractError(`M4.6 playback chunk ${index}: нарушена размерность.`);
}
const chunkSha256 = text(row.sha256, `M4.6 playback chunk ${index}.SHA-256`);
if (!/^[a-f0-9]{64}$/.test(chunkSha256)) {
throw new M4ThreatContractError(`M4.6 playback chunk ${index}: SHA-256 недопустим.`);
}
const expectedUrl = `${endpointRoot}/${result}/timeline/playback/chunks/${index}`;
const url = text(row.url, `M4.6 playback chunk ${index}.URL`);
if (url !== expectedUrl || url.includes("worker")) {
throw new M4ThreatContractError(`M4.6 playback chunk ${index}: разрешён только sealed local API.`);
}
return {
index,
start,
count,
pointStart,
pointCount: pointChunkCount,
url,
mediaType: "application/octet-stream",
byteLength: chunkBytes,
sha256: chunkSha256,
};
});
if (!response.ok) throw new M4ThreatContractError(`M4.6 playback points: HTTP ${response.status}.`);
const bytes = new Uint8Array(byteLength);
if (response.body) {
const reader = response.body.getReader();
let offset = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (offset + value.byteLength > bytes.byteLength) {
throw new M4ThreatContractError("M4.6 playback points превысил объявленный размер.");
}
bytes.set(value, offset);
offset += value.byteLength;
onProgress?.({ phase: "download", loadedBytes: offset, totalBytes: byteLength });
}
if (offset !== byteLength) {
throw new M4ThreatContractError("M4.6 playback points получен не полностью.");
}
} else {
const fallback = new Uint8Array(await response.arrayBuffer());
if (fallback.byteLength !== byteLength) {
throw new M4ThreatContractError("M4.6 playback points: неверный размер.");
}
bytes.set(fallback);
}
onProgress?.({ phase: "verify", loadedBytes: byteLength, totalBytes: byteLength });
if (await playbackSha256Hex(bytes.buffer) !== sha256) {
throw new M4ThreatContractError("M4.6 playback points: SHA-256 не совпал.");
}
onProgress?.({ phase: "ready", loadedBytes: byteLength, totalBytes: byteLength });
return {
resultId: result,
frameCount,
pointCount,
pointOffsets,
pointsMapXyzM: new Float32Array(bytes.buffer),
chunkFrameCount,
residentChunkCountMax,
forwardPrefetchChunkCount,
chunks,
track: { url: trackUrl, byteLength, sha256 },
};
}
export async function fetchM4ThreatPlaybackPointChunk(
manifest: M4ThreatPlaybackManifest,
chunkIndex: number,
{
fetcher = fetch,
signal,
onProgress,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
onProgress?: (progress: M4ThreatPlaybackProgress) => void;
} = {},
): Promise<M4ThreatPlaybackPointPack> {
const descriptor = manifest.chunks[chunkIndex];
if (!descriptor || descriptor.index !== chunkIndex) {
throw new M4ThreatContractError("M4.6 playback chunk отсутствует.");
}
onProgress?.({ phase: "download", loadedBytes: 0, totalBytes: descriptor.byteLength });
const response = await fetcher(descriptor.url, {
headers: { Accept: "application/octet-stream" },
signal,
});
if (!response.ok) {
throw new M4ThreatContractError(`M4.6 playback chunk: HTTP ${response.status}.`);
}
const buffer = await response.arrayBuffer();
if (buffer.byteLength !== descriptor.byteLength) {
throw new M4ThreatContractError("M4.6 playback chunk: неверный размер.");
}
onProgress?.({
phase: "verify",
loadedBytes: descriptor.byteLength,
totalBytes: descriptor.byteLength,
});
if (await playbackSha256Hex(buffer) !== descriptor.sha256) {
throw new M4ThreatContractError("M4.6 playback chunk: SHA-256 не совпал.");
}
onProgress?.({
phase: "ready",
loadedBytes: descriptor.byteLength,
totalBytes: descriptor.byteLength,
});
return {
resultId: manifest.resultId,
frameCount: manifest.frameCount,
pointCount: descriptor.pointCount,
pointOffsets: manifest.pointOffsets,
pointsMapXyzM: new Float32Array(buffer),
pointStart: descriptor.pointStart,
startSequence: descriptor.start,
sequenceCount: descriptor.count,
};
}
export async function fetchM4ThreatPlaybackPointPack(
result: string,
{
fetcher = fetch,
signal,
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
onProgress,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
endpointRoot?: string;
onProgress?: (progress: M4ThreatPlaybackProgress) => void;
} = {},
): Promise<M4ThreatPlaybackPointPack> {
onProgress?.({ phase: "manifest", loadedBytes: 0, totalBytes: 0 });
const manifest = await fetchM4ThreatPlaybackManifest(result, {
fetcher,
signal,
endpointRoot,
});
const response = await fetcher(manifest.track.url, {
headers: { Accept: "application/octet-stream" },
signal,
});
if (!response.ok) throw new M4ThreatContractError(`M4.6 playback points: HTTP ${response.status}.`);
const buffer = await response.arrayBuffer();
if (buffer.byteLength !== manifest.track.byteLength) {
throw new M4ThreatContractError("M4.6 playback points: неверный размер.");
}
onProgress?.({ phase: "verify", loadedBytes: buffer.byteLength, totalBytes: buffer.byteLength });
if (await playbackSha256Hex(buffer) !== manifest.track.sha256) {
throw new M4ThreatContractError("M4.6 playback points: SHA-256 не совпал.");
}
onProgress?.({ phase: "ready", loadedBytes: buffer.byteLength, totalBytes: buffer.byteLength });
return {
resultId: manifest.resultId,
frameCount: manifest.frameCount,
pointCount: manifest.pointCount,
pointOffsets: manifest.pointOffsets,
pointsMapXyzM: new Float32Array(buffer),
};
}
@@ -236,9 +236,10 @@
box-sizing: border-box;
min-width: 0;
align-items: stretch;
width: max-content;
max-width: calc(var(--m4-replay-threat-overlay-pane-width, 100%) - 1.2rem);
grid-template-columns: minmax(0, max-content);
width: min(17.5rem, calc(var(--m4-replay-threat-overlay-pane-width, 100%) - 1.2rem));
max-width: none;
grid-auto-rows: 3.8rem;
grid-template-columns: minmax(0, 1fr);
gap: 0.3rem;
overflow: hidden;
background: transparent;
@@ -247,6 +248,9 @@
}
.m4-replay-threat-visual__overlay > div {
box-sizing: border-box;
height: 3.8rem;
grid-template-rows: auto auto minmax(1.32rem, 1fr);
border-radius: var(--nodedc-radius-control-compact);
background: color-mix(in srgb, var(--nodedc-floating-surface) 80%, transparent);
padding: 0.55rem 0.65rem;
@@ -254,15 +258,24 @@
}
.m4-replay-threat-visual__overlay span,
.m4-replay-threat-visual__overlay strong,
.m4-replay-threat-visual__overlay small {
.m4-replay-threat-visual__overlay strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.m4-replay-threat-visual__overlay small {
display: -webkit-box;
min-height: 1.32rem;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
}
@media (max-width: 900px) {
.m4-replay-threat-visual__overlay {
width: min(17.5rem, calc(100% - 1.2rem));
max-width: calc(100% - 1.2rem);
}
}
@@ -372,3 +385,13 @@
background: var(--nodedc-text-muted);
opacity: 0.42;
}
.laboratory-metric-evidence-scene__legend span[data-decision="performance"] {
font-variant-numeric: tabular-nums;
}
.laboratory-metric-evidence-scene__legend span[data-decision="performance"]::before {
box-sizing: border-box;
border: 1px solid rgb(var(--nodedc-accent-rgb));
background: transparent;
}
@@ -8,13 +8,16 @@ import {
fetchE47SemanticSlamResult,
type E47SemanticSlamResult,
} from "../../core/laboratory/e47SemanticSlam";
import {
fetchM49PhysicalSafetyPlaybackIdForSource,
M49PhysicalSafetyPlaybackBuffer,
type M49PhysicalSafetyPlaybackProgress,
} from "../../core/laboratory/m49PhysicalSafetyPlayback";
import {
fetchM49TgsFullShadowPlaybackPack,
type M49TgsFullShadowResult,
type M49TgsFullShadowPlaybackPack,
type M49TgsFullShadowPlaybackProgress,
type M49TgsFullShadowSpatial,
type M49TgsFullShadowStateCode,
} from "../../core/laboratory/m49TgsFullShadow";
import {
M4ReplayThreatVisual,
@@ -33,13 +36,6 @@ const PALETTE: readonly RecordedEvidenceSemanticPaletteEntry[] = [
{ classId: 3, color: { kind: "token", token: "--nodedc-warning-rgb" } },
];
function cellState(code: M49TgsFullShadowStateCode): M4ReplayClassifiedSpatialFrame["cellsMapGravityLocal"][number]["state"] {
if (code === 1) return "ground-support";
if (code === 2) return "nonground-occupied";
if (code === 3) return "unknown-rejected";
return "unobserved";
}
function message(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
@@ -51,6 +47,9 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
const [semantic, setSemantic] = useState<E47SemanticSlamResult | null>(null);
const [semanticError, setSemanticError] = useState<string | null>(null);
const [playbackPack, setPlaybackPack] = useState<M49TgsFullShadowPlaybackPack | null>(null);
const [physicalPlayback, setPhysicalPlayback] = useState<M49PhysicalSafetyPlaybackBuffer | null>(null);
const [physicalProgress, setPhysicalProgress] = useState<M49PhysicalSafetyPlaybackProgress | null>(null);
const [physicalRevision, setPhysicalRevision] = useState(0);
const [playbackProgress, setPlaybackProgress] = useState<M49TgsFullShadowPlaybackProgress>({
phase: "manifest",
trackId: null,
@@ -83,12 +82,27 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
useEffect(() => {
const controller = new AbortController();
setPlaybackPack(null);
setPhysicalPlayback(null);
setPhysicalProgress(null);
setPhysicalRevision(0);
setPlaybackProgress({ phase: "manifest", trackId: null, loadedBytes: 0, totalBytes: 0 });
setError(null);
void fetchM49TgsFullShadowPlaybackPack(result.resultId, {
void fetchM49PhysicalSafetyPlaybackIdForSource(result.resultId, {
signal: controller.signal,
onProgress: setPlaybackProgress,
}).then((pack) => {
}).then(async (physicalResultId) => {
if (controller.signal.aborted) return;
if (physicalResultId) {
const playback = await M49PhysicalSafetyPlaybackBuffer.open(physicalResultId, {
signal: controller.signal,
onProgress: setPhysicalProgress,
});
if (!controller.signal.aborted) setPhysicalPlayback(playback);
return;
}
const pack = await fetchM49TgsFullShadowPlaybackPack(result.resultId, {
signal: controller.signal,
onProgress: setPlaybackProgress,
});
if (!controller.signal.aborted) setPlaybackPack(pack);
}).catch((caught: unknown) => {
if (!controller.signal.aborted) setError(message(caught));
@@ -96,69 +110,84 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
return () => controller.abort();
}, [result.resultId]);
const spatial = useMemo<M49TgsFullShadowSpatial | null>(() => {
if (activeSequence === null || !playbackPack) return null;
useEffect(() => {
if (activeSequence === null || !physicalPlayback) return;
let active = true;
const resident = physicalPlayback.frameIfResident(activeSequence);
void physicalPlayback.prepare(activeSequence).then(() => {
if (active && !resident) setPhysicalRevision((value) => value + 1);
}).catch((caught: unknown) => {
if (active) setError(message(caught));
});
return () => {
active = false;
};
}, [activeSequence, physicalPlayback]);
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
if (activeSequence === null) return null;
if (physicalPlayback) {
const frame = physicalPlayback.frameIfResident(activeSequence);
if (!frame) return null;
return {
sourceSequence: activeSequence,
sampleAvailable: frame.metadata.sampleAvailable,
sourcePointCount: frame.metadata.metrics.eligible_point_count ?? 0,
pointsMapGravityLocalXyzM: [],
pointClassIds: [],
cellsMapGravityLocal: [],
packedCellsMapGravityLocal: {
centersXyM: frame.centersXyM,
stateCodes: frame.states,
zBoundsM: frame.zBoundsM,
},
cellSizeM: frame.cellSizeM,
classes: CLASSES,
palette: PALETTE,
};
}
if (!playbackPack) return null;
const frame = playbackPack.frames[activeSequence];
if (!frame) return null;
const cellOffset = activeSequence * playbackPack.cellCount;
const zOffset = cellOffset * 2;
const states = Array.from(
playbackPack.states.subarray(cellOffset, cellOffset + playbackPack.cellCount),
(value) => value as M49TgsFullShadowStateCode,
);
const zBoundsM = Array.from({ length: playbackPack.cellCount }, (_, index) => {
const minimum = playbackPack.zBoundsM[zOffset + index * 2]!;
const maximum = playbackPack.zBoundsM[zOffset + index * 2 + 1]!;
return [
Number.isFinite(minimum) ? minimum : null,
Number.isFinite(maximum) ? maximum : null,
] as const;
});
return {
resultId: playbackPack.resultId,
sourceSequence: activeSequence,
sourceFrameIndex: frame.sourceFrameIndex,
sessionSeconds: frame.sessionSeconds,
sampleAvailable: frame.sampleAvailable,
costmap: {
cellSizeM: result.configuration.cellSizeM,
radiusM: result.configuration.radiusM,
sourcePointCount: frame.metrics.eligiblePointCount,
pointsMapGravityLocalXyzM: [],
pointClassIds: [],
cellsMapGravityLocal: [],
packedCellsMapGravityLocal: {
centersXyM: playbackPack.centersXyM,
states,
zBoundsM,
stateCodes: playbackPack.states.subarray(
cellOffset,
cellOffset + playbackPack.cellCount,
),
zBoundsM: playbackPack.zBoundsM.subarray(
zOffset,
zOffset + playbackPack.cellCount * 2,
),
},
metrics: frame.metrics,
cellSizeM: result.configuration.cellSizeM,
classes: CLASSES,
palette: PALETTE,
};
}, [activeSequence, playbackPack, result.configuration.cellSizeM, result.configuration.radiusM]);
const loading = activeSequence !== null && !spatial && !error;
}, [activeSequence, physicalPlayback, physicalRevision, playbackPack, result.configuration.cellSizeM]);
const loading = activeSequence !== null && !classifiedFrame && !error;
const progressPercent = playbackProgress.totalBytes > 0
? Math.min(100, Math.round((playbackProgress.loadedBytes / playbackProgress.totalBytes) * 100))
: 0;
const loadingLabel = playbackProgress.phase === "manifest"
const loadingLabel = physicalProgress
? physicalProgress.phase === "ready"
? "Sealed TGS playback готов"
: `Буферизуем sealed TGS · ${physicalProgress.residentChunkIndexes.length}/3 chunks · ${(physicalProgress.loadedBytes / 1_048_576).toFixed(1)} МБ`
: playbackProgress.phase === "manifest"
? "Проверяем playback manifest"
: playbackProgress.phase === "ready"
? "TGS playback готов"
: `Подготавливаем TGS playback · ${progressPercent}% · ${(playbackProgress.loadedBytes / 1_048_576).toFixed(1)}/${(playbackProgress.totalBytes / 1_048_576).toFixed(1)} МБ`;
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
if (!spatial) return null;
return {
sourceSequence: spatial.sourceSequence,
sampleAvailable: spatial.sampleAvailable,
sourcePointCount: spatial.metrics.eligiblePointCount,
pointsMapGravityLocalXyzM: [],
pointClassIds: [],
cellsMapGravityLocal: spatial.costmap.centersXyM.map((center, index) => ({
centerXyM: center,
zBoundsM: spatial.costmap.zBoundsM[index]!,
state: cellState(spatial.costmap.states[index]!),
})),
cellSizeM: spatial.costmap.cellSizeM,
classes: CLASSES,
palette: PALETTE,
};
}, [spatial]);
const handleSequenceChange = useCallback((sequence: number | null) => {
setActiveSequence(sequence);
}, []);
@@ -14,6 +14,7 @@ import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricCellEvidence,
type LaboratoryMetricEvidenceSceneHandle,
type LaboratoryMetricPackedCellEvidence,
type LaboratoryMetricSceneMode,
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
@@ -118,6 +119,11 @@ export interface M4ReplayClassifiedSpatialFrame {
zBoundsM: readonly [number | null, number | null];
state: LaboratoryMetricCellEvidence["state"];
}[];
packedCellsMapGravityLocal?: {
centersXyM: Float32Array;
zBoundsM: Float32Array;
stateCodes: Uint8Array;
};
cellSizeM: number;
classes: readonly RecordedEvidenceSemanticClass[];
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
@@ -414,19 +420,41 @@ export function M4ReplayThreatVisual({
resultId: string;
frame: M4ReplayClassifiedSpatialFrame;
} | null>(null);
if (classifiedSpatialLayer?.frame) {
lastClassifiedSpatialFrameRef.current = { resultId, frame: classifiedSpatialLayer.frame };
const incomingClassifiedSpatialFrame = classifiedSpatialLayer?.frame ?? null;
if (incomingClassifiedSpatialFrame && incomingClassifiedSpatialFrame.sampleAvailable !== false) {
lastClassifiedSpatialFrameRef.current = { resultId, frame: incomingClassifiedSpatialFrame };
}
const lastAvailableClassifiedSpatialFrame = lastClassifiedSpatialFrameRef.current?.resultId === resultId
? lastClassifiedSpatialFrameRef.current.frame
: null;
// `undefined !== false` is true, so the optional-chain form here used to
// select `null` during every short chunk miss. That unmounted the WebGL
// scene, flashed the alert state and recreated OrbitControls at the default
// view on the next frame. Keep the last complete volume until the exact
// classified frame is resident again.
const displayedClassifiedSpatialFrame = classifiedSpatialFrame
?? (lastClassifiedSpatialFrameRef.current?.resultId === resultId
? lastClassifiedSpatialFrameRef.current.frame
: null);
&& classifiedSpatialFrame.sampleAvailable !== false
? classifiedSpatialFrame
: lastAvailableClassifiedSpatialFrame ?? classifiedSpatialFrame;
const displayedClassifiedFrameHeld = Boolean(
classifiedSpatialFrame
&& displayedClassifiedSpatialFrame
&& classifiedSpatialFrame.sourceSequence !== displayedClassifiedSpatialFrame.sourceSequence,
);
const classifiedContextSpatialFrame = displayedClassifiedSpatialFrame
? timelineFrame.availableFrames.find(
(candidate) => candidate.spatialAvailable
&& candidate.sequence === displayedClassifiedSpatialFrame.sourceSequence,
) ?? (spatialFrame?.sequence === displayedClassifiedSpatialFrame.sourceSequence
? spatialFrame
: null)
: null;
const replaceClassifiedPointCloud = classifiedSpatialLayer?.replacePointCloud ?? true;
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
const mapGravityLocalSensorToBodyGround = useCallback((
point: readonly [number, number, number],
): readonly [number, number, number] => {
const basis = activeSpatialFrame?.bodyFrame?.basisMapFromBody;
const basis = classifiedContextSpatialFrame?.bodyFrame?.basisMapFromBody;
const rotated: readonly [number, number, number] = basis ? [
basis[0][0] * point[0] + basis[1][0] * point[1] + basis[2][0] * point[2],
basis[0][1] * point[0] + basis[1][1] * point[1] + basis[2][1] * point[2],
@@ -435,7 +463,7 @@ export function M4ReplayThreatVisual({
// TGS evidence is translation-only map-gravity-local with the current LiDAR
// as its origin. The metric scene uses the body ground projection as z=0.
return [rotated[0], rotated[1], rotated[2] + nominalSensorHeightM];
}, [activeSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
}, [classifiedContextSpatialFrame?.bodyFrame?.basisMapFromBody, nominalSensorHeightM]);
const classifiedPointsBody = useMemo(
() => displayedClassifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
mapGravityLocalSensorToBodyGround,
@@ -465,20 +493,58 @@ export function M4ReplayThreatVisual({
}) ?? [],
[displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM],
);
const classifiedCellCounts = useMemo(() => ({
ground: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
(cell) => cell.state === "ground-support",
).length ?? 0,
occupied: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
(cell) => cell.state === "nonground-occupied",
).length ?? 0,
rejected: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
(cell) => cell.state === "unknown-rejected",
).length ?? 0,
unobserved: classifiedSpatialFrame?.cellsMapGravityLocal.filter(
(cell) => cell.state === "unobserved",
).length ?? 0,
}), [classifiedSpatialFrame]);
const classifiedPackedCellsBody = useMemo<LaboratoryMetricPackedCellEvidence | undefined>(() => {
const packed = displayedClassifiedSpatialFrame?.packedCellsMapGravityLocal;
if (!packed) return undefined;
const cellCount = packed.stateCodes.length;
if (packed.centersXyM.length !== cellCount * 2 || packed.zBoundsM.length !== cellCount * 2) {
return undefined;
}
const centersBodyXyM = new Float32Array(cellCount * 2);
const zBoundsM = new Float32Array(cellCount * 2);
for (let index = 0; index < cellCount; index += 1) {
const body = mapGravityLocalSensorToBodyGround([
packed.centersXyM[index * 2]!,
packed.centersXyM[index * 2 + 1]!,
0,
]);
centersBodyXyM[index * 2] = body[0];
centersBodyXyM[index * 2 + 1] = body[1];
const minimum = packed.zBoundsM[index * 2]!;
const maximum = packed.zBoundsM[index * 2 + 1]!;
zBoundsM[index * 2] = Number.isFinite(minimum)
? minimum + nominalSensorHeightM
: Number.NaN;
zBoundsM[index * 2 + 1] = Number.isFinite(maximum)
? maximum + nominalSensorHeightM
: Number.NaN;
}
return { centersBodyXyM, zBoundsM, stateCodes: packed.stateCodes };
}, [displayedClassifiedSpatialFrame, mapGravityLocalSensorToBodyGround, nominalSensorHeightM]);
const classifiedCellCounts = useMemo(() => {
const counts = { ground: 0, occupied: 0, rejected: 0, unobserved: 0 };
const packed = classifiedSpatialFrame?.packedCellsMapGravityLocal;
if (packed) {
for (const code of packed.stateCodes) {
if (code === 1) counts.ground += 1;
else if (code === 2) counts.occupied += 1;
else if (code === 3) counts.rejected += 1;
else if (code === 0) counts.unobserved += 1;
else counts.rejected += 1;
}
return counts;
}
for (const cell of classifiedSpatialFrame?.cellsMapGravityLocal ?? []) {
if (cell.state === "ground-support") counts.ground += 1;
else if (cell.state === "nonground-occupied") counts.occupied += 1;
else if (cell.state === "unknown-rejected") counts.rejected += 1;
else counts.unobserved += 1;
}
return counts;
}, [classifiedSpatialFrame]);
const classifiedCellCount = classifiedSpatialFrame?.packedCellsMapGravityLocal?.stateCodes.length
?? classifiedSpatialFrame?.cellsMapGravityLocal.length
?? 0;
const sceneObstacles = useMemo(() => spatialFrame?.metricObstacles.map((obstacle) => ({
id: obstacle.componentId,
decision: obstacle.assessment.decision,
@@ -840,14 +906,16 @@ export function M4ReplayThreatVisual({
<strong>{classifiedSpatialLayer
? classifiedSpatialFrame
? replaceClassifiedPointCloud
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} cells`
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} TGS cells`
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedCellCount.toLocaleString("ru-RU")} cells`
: `${(activeSpatialFrame?.pointCloudSourceCount ?? classifiedSpatialFrame.sourcePointCount ?? 0).toLocaleString("ru-RU")} source points · ${classifiedCellCount.toLocaleString("ru-RU")} TGS cells`
: "TGS spatial buffer"
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
<small>{classifiedSpatialLayer
? classifiedSpatialFrame
? classifiedSpatialFrame.sampleAvailable === false
? "LiDAR отсутствует · все ячейки принудительно UNOBSERVED · causal rolling 1 s"
? displayedClassifiedFrameHeld && displayedClassifiedSpatialFrame
? `LiDAR отсутствует · current UNOBSERVED · объёмный контекст удержан с frame ${displayedClassifiedSpatialFrame.sourceSequence + 1}`
: "LiDAR отсутствует · все ячейки принудительно UNOBSERVED · causal rolling 1 s"
: activeSpatialFrame
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
: "TGS рассчитан · linked source cloud недоступен для этого кадра"
@@ -987,37 +1055,36 @@ export function M4ReplayThreatVisual({
</div>
</div>
) : null}
{(!classifiedSpatialLayer ? spatialFrame : displayedClassifiedSpatialFrame) ? (
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedPointsBody
: activeSpatialFrame?.pointCloudBodyXyzM ?? []}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={displayedClassifiedSpatialFrame ? [] : sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
mode={spatialMode}
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
showLowStep={displayedClassifiedSpatialFrame ? false : showLowStep}
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.pointClassIds
: alignedSemanticPointIds}
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.classes
: semanticClasses}
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.palette
: semanticPalette}
classifiedCells={classifiedCellsBody}
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
showClassifiedCells={showRollingMap}
/>
) : null}
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? classifiedPointsBody
: classifiedContextSpatialFrame?.pointCloudBodyXyzM ?? []}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={classifiedSpatialLayer ? [] : sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={displayedClassifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
mode={spatialMode}
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showRollingMap={showRollingMap}
showLowStep={classifiedSpatialLayer ? false : showLowStep}
pointSemanticClassIds={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.pointClassIds
: alignedSemanticPointIds}
semanticClasses={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.classes
: semanticClasses}
semanticPalette={displayedClassifiedSpatialFrame && replaceClassifiedPointCloud
? displayedClassifiedSpatialFrame.palette
: semanticPalette}
classifiedCells={classifiedCellsBody}
classifiedPackedCells={classifiedPackedCellsBody}
classifiedCellSizeM={displayedClassifiedSpatialFrame?.cellSizeM}
showClassifiedCells={showRollingMap}
/>
{classifiedSpatialLayer && !displayedClassifiedSpatialFrame ? (
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
{classifiedSpatialLayer.loading || displayingBufferedFrame
@@ -1033,7 +1100,10 @@ export function M4ReplayThreatVisual({
) : null}
{classifiedSpatialFrame?.sampleAvailable === false ? (
<div className="m4-replay-threat-visual__pane-status" role="status">
Кадр {classifiedSpatialFrame.sourceSequence + 1}: LiDAR отсутствует; все 2 244 TGS-ячейки явно UNOBSERVED.
Кадр {classifiedSpatialFrame.sourceSequence + 1}: LiDAR отсутствует; current safety все {classifiedCellCount.toLocaleString("ru-RU")} TGS-ячейки UNOBSERVED
{displayedClassifiedFrameHeld && displayedClassifiedSpatialFrame
? `; для ориентации удержан последний объёмный контекст кадра ${displayedClassifiedSpatialFrame.sourceSequence + 1}.`
: "."}
</div>
) : classifiedSpatialFrame && !activeSpatialFrame ? (
<div className="m4-replay-threat-visual__pane-status" role="status">
@@ -2,13 +2,14 @@ import { useEffect, useMemo, useRef, useState } from "react";
import {
fetchM4ThreatCameraPointOverlay,
fetchM4ThreatPlaybackPointPack,
fetchM4ThreatPlaybackManifest,
fetchM4ThreatPlaybackPointChunk,
fetchM4ThreatTimeline,
fetchM4ThreatTimelineChunk,
M4_THREAT_TIMELINE_ENDPOINT_ROOT,
selectM4ThreatTimelineSequence,
type M4ThreatCameraPointOverlay,
type M4ThreatPlaybackPointPack,
type M4ThreatPlaybackManifest,
type M4ThreatPlaybackProgress,
type M4ThreatTimeline,
type M4ThreatTimelineChunk,
@@ -86,7 +87,7 @@ export function useM4ThreatTimelineFrame({
() => new Map(),
);
const [error, setError] = useState<string | null>(null);
const [pointPack, setPointPack] = useState<M4ThreatPlaybackPointPack | null>(null);
const [playbackManifest, setPlaybackManifest] = useState<M4ThreatPlaybackManifest | null>(null);
const [playbackProgress, setPlaybackProgress] = useState<M4ThreatPlaybackProgress>({
phase: "manifest",
loadedBytes: 0,
@@ -102,7 +103,7 @@ export function useM4ThreatTimelineFrame({
useEffect(() => {
const controller = new AbortController();
setPointPack(null);
setPlaybackManifest(null);
setPlaybackError(null);
setPlaybackProgress({ phase: "manifest", loadedBytes: 0, totalBytes: 0 });
if (!timeline) return () => controller.abort();
@@ -110,15 +111,15 @@ export function useM4ThreatTimelineFrame({
setPlaybackProgress({ phase: "ready", loadedBytes: 0, totalBytes: 0 });
return () => controller.abort();
}
void fetchM4ThreatPlaybackPointPack(resultId, {
void fetchM4ThreatPlaybackManifest(resultId, {
signal: controller.signal,
endpointRoot,
onProgress: (progress) => {
if (!controller.signal.aborted) setPlaybackProgress(progress);
},
})
.then((pack) => {
if (!controller.signal.aborted) setPointPack(pack);
.then((manifest) => {
if (!controller.signal.aborted) {
setPlaybackManifest(manifest);
setPlaybackProgress({ phase: "ready", loadedBytes: 0, totalBytes: 0 });
}
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) {
@@ -157,7 +158,7 @@ export function useM4ThreatTimelineFrame({
activeChunkStartRef.current = activeChunkStart;
useEffect(() => {
if (!timeline || activeChunkStart === null || (binaryPlayback && !pointPack)) return;
if (!timeline || activeChunkStart === null || (binaryPlayback && !playbackManifest)) return;
const starts = m4ThreatChunkWindowStarts(
activeChunkStart,
chunkSize,
@@ -168,12 +169,28 @@ export function useM4ThreatTimelineFrame({
if (chunksRef.current.has(start) || inFlight.current.has(start)) continue;
const controller = new AbortController();
inFlight.current.set(start, controller);
void fetchM4ThreatTimelineChunk(resultId, start, chunkSize, {
signal: controller.signal,
endpointRoot,
cameraObstacleProjectionDelivery: timeline.cameraObstacleProjectionDelivery,
playbackPointPack: binaryPlayback ? pointPack ?? undefined : undefined,
})
void (async () => {
const playbackPointPack = binaryPlayback && playbackManifest
? await fetchM4ThreatPlaybackPointChunk(
playbackManifest,
Math.floor(start / playbackManifest.chunkFrameCount),
{
signal: controller.signal,
onProgress: (progress) => {
if (!controller.signal.aborted && start === activeChunkStartRef.current) {
setPlaybackProgress(progress);
}
},
},
)
: undefined;
return fetchM4ThreatTimelineChunk(resultId, start, chunkSize, {
signal: controller.signal,
endpointRoot,
cameraObstacleProjectionDelivery: timeline.cameraObstacleProjectionDelivery,
playbackPointPack,
});
})()
.then((chunk) => {
if (controller.signal.aborted) return;
setChunks((current) => {
@@ -203,7 +220,7 @@ export function useM4ThreatTimelineFrame({
// loaded first, then the next chunk is prefetched on the following render.
break;
}
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, pointPack, resultId, timeline]);
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, playbackManifest, resultId, timeline]);
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
if (activeSequence === null || activeChunkStart === null) return null;
@@ -230,8 +247,8 @@ export function useM4ThreatTimelineFrame({
? "Проверяем M4 playback manifest"
: playbackProgress.phase === "ready"
? "Подготавливаем текущий spatial-кадр"
: `${playbackProgress.phase === "verify" ? "Проверяем" : "Загружаем"} M4 spatial playback · ${playbackProgress.totalBytes > 0
? `${Math.min(100, Math.round(playbackProgress.loadedBytes / playbackProgress.totalBytes * 100))}% · ${(playbackProgress.loadedBytes / 1_048_576).toFixed(1)}/${(playbackProgress.totalBytes / 1_048_576).toFixed(1)} МБ`
: `${playbackProgress.phase === "verify" ? "Проверяем" : "Буферизуем"} текущий spatial chunk · ${playbackProgress.totalBytes > 0
? `${Math.min(100, Math.round(playbackProgress.loadedBytes / playbackProgress.totalBytes * 100))}% · ${(playbackProgress.loadedBytes / 1_048_576).toFixed(1)} МБ`
: "0%"}`,
error: playbackError ?? error,
};
@@ -691,3 +691,18 @@ test("metric evidence keeps one WebGL renderer while frame labels advance", () =
/querySelector\("canvas"\)[\s\S]*?setAttribute\("aria-label", label\)[\s\S]*?\}, \[label\]\);/,
);
});
test("metric evidence renders on demand while preserving damped camera interaction", () => {
const source = readFileSync(
new URL("../src/components/laboratory/LaboratoryMetricEvidenceScene.tsx", import.meta.url),
"utf8",
);
assert.match(source, /const requestRenderRef = useRef/);
assert.match(source, /controls\.addEventListener\("change", requestRender\)/);
assert.match(source, /requestRenderRef\.current\(\)/);
assert.doesNotMatch(
source,
/const render = \(\) => \{\s*animationFrame = window\.requestAnimationFrame\(render\)/,
);
});
@@ -0,0 +1,234 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let M49PhysicalSafetyPlaybackBuffer;
let fetchM49PhysicalSafetyPlaybackIdForSource;
const resultId = `m49-physical-safety-playback-${"a".repeat(64)}`;
const endpoint = `/api/v1/laboratory/m49/physical-safety-playback/${resultId}`;
const frameCount = 160;
const cellCount = 4;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({
M49PhysicalSafetyPlaybackBuffer,
fetchM49PhysicalSafetyPlaybackIdForSource,
} = await server.ssrLoadModule(
"/src/core/laboratory/m49PhysicalSafetyPlayback.ts",
));
});
after(async () => {
await server?.close();
});
function sha256(buffer) {
return createHash("sha256").update(Buffer.from(buffer)).digest("hex");
}
function chunk(index) {
const start = index * 32;
const count = Math.min(32, frameCount - start);
const stateBytes = count * cellCount;
const zBytes = count * cellCount * 2 * 4;
const headerBytes = 28;
const padding = (4 - ((headerBytes + stateBytes) % 4)) % 4;
const buffer = new ArrayBuffer(headerBytes + stateBytes + padding + zBytes);
const bytes = new Uint8Array(buffer);
bytes.set(new TextEncoder().encode("MCPSCH01"));
const view = new DataView(buffer);
view.setUint32(8, start, true);
view.setUint32(12, count, true);
view.setUint32(16, cellCount, true);
view.setUint32(20, stateBytes, true);
view.setUint32(24, zBytes, true);
const states = new Uint8Array(buffer, headerBytes, stateBytes);
states.fill(1);
const z = new Float32Array(buffer, headerBytes + stateBytes + padding, count * cellCount * 2);
for (let local = 0; local < count; local += 1) {
const sequence = start + local;
for (let cell = 0; cell < cellCount; cell += 1) {
z[(local * cellCount + cell) * 2] = 0;
z[(local * cellCount + cell) * 2 + 1] = 0.12;
}
if (sequence === 17) {
states.fill(0, local * cellCount, (local + 1) * cellCount);
z.fill(Number.NaN, local * cellCount * 2, (local + 1) * cellCount * 2);
}
}
return buffer;
}
function fixture() {
const centers = new Float32Array([0, 0, 0.15, 0, 0.3, 0, 0.45, 0]).buffer;
const frames = new TextEncoder().encode(Array.from({ length: frameCount }, (_, sequence) => (
JSON.stringify({
source_frame_index: sequence,
session_seconds: sequence / 10,
sample_available: sequence !== 17,
eligible_point_count: sequence === 17 ? 0 : cellCount,
})
)).join("\n") + "\n").buffer;
const chunks = Array.from({ length: Math.ceil(frameCount / 32) }, (_, index) => chunk(index));
const manifest = {
schema_version: "missioncore.m49-physical-safety-playback/v1",
result_id: resultId,
created_at_utc: "2026-08-27T12:00:00Z",
access: "read-only-sealed-local",
identity: { source_result_id: `m49-tgs-full-shadow-${"b".repeat(64)}` },
execution: {
execution_class: "local-sequential-offline",
worker_role: "realtime-only",
worker_runtime_dependency: false,
worker_requests_required: 0,
},
authority: {
commands_enabled: false,
navigation_or_safety_accepted: false,
actuation_accepted: false,
},
playback: {
coordinate_frame: "map-gravity-local",
source_pace_hz: 10,
frame_count: frameCount,
cell_count: cellCount,
cell_size_m: 0.15,
radius_m: 12,
state_codes: {
UNOBSERVED: 0,
GROUND_SUPPORT: 1,
NONGROUND_OCCUPIED: 2,
UNKNOWN_REJECTED: 3,
},
chunk_frame_count: 32,
startup_prebuffer_chunk_count: 2,
resident_chunk_count_max: 3,
forward_prefetch_chunk_count: 1,
centers: {
url: `${endpoint}/tracks/centers`,
media_type: "application/octet-stream",
byte_length: centers.byteLength,
sha256: sha256(centers),
dtype: "<f4",
shape: [cellCount, 2],
},
frames: {
url: `${endpoint}/tracks/frames`,
media_type: "application/x-ndjson",
byte_length: frames.byteLength,
sha256: sha256(frames),
dtype: "ndjson",
shape: [frameCount],
},
chunks: chunks.map((buffer, index) => ({
index,
start: index * 32,
count: Math.min(32, frameCount - index * 32),
url: `${endpoint}/chunks/${index}`,
media_type: "application/octet-stream",
byte_length: buffer.byteLength,
sha256: sha256(buffer),
header_bytes: 28,
format: "mcpsch01-states-u8-aligned-z-bounds-f32le",
})),
},
};
const responses = new Map([
[`${endpoint}/tracks/centers`, centers],
[`${endpoint}/tracks/frames`, frames],
...chunks.map((buffer, index) => [`${endpoint}/chunks/${index}`, buffer]),
]);
return { manifest, responses };
}
test("physical-safety playback opens and seeks with bounded local chunks only", async () => {
const { manifest, responses } = fixture();
const requested = [];
const progress = [];
const fetcher = async (input) => {
const url = String(input);
requested.push(url);
if (url === `${endpoint.replace(`/${resultId}`, "")}/results?limit=2&source_result_id=${manifest.identity.source_result_id}`) {
return Response.json({
schema_version: "missioncore.m49-physical-safety-playback-catalog/v1",
worker_runtime_dependency: false,
access: "read-only-sealed-local",
items: [{
result_id: resultId,
source_result_id: manifest.identity.source_result_id,
worker_runtime_dependency: false,
navigation_or_safety_accepted: false,
}],
});
}
if (url === `${endpoint}/manifest`) {
return Response.json(manifest);
}
const payload = responses.get(url);
return payload
? new Response(payload, { headers: { "Content-Type": "application/octet-stream" } })
: new Response(null, { status: 404 });
};
const resolvedResultId = await fetchM49PhysicalSafetyPlaybackIdForSource(
manifest.identity.source_result_id,
{ fetcher },
);
assert.equal(resolvedResultId, resultId);
const playback = await M49PhysicalSafetyPlaybackBuffer.open(resolvedResultId, {
fetcher,
onProgress: (value) => progress.push(value),
});
assert.deepEqual(playback.residentChunkIndexes, [0, 1]);
assert.equal(progress.at(-1).phase, "ready");
assert.equal(requested.some((url) => url.includes("worker")), false);
const startupRequestCount = requested.length;
await playback.prepare(0);
await playback.prepare(0);
assert.equal(requested.length, startupRequestCount);
const missing = await playback.frame(17);
assert.equal(missing.metadata.sampleAvailable, false);
assert.deepEqual(new Set(missing.states), new Set([0]));
assert.equal([...missing.zBoundsM].every((value) => Number.isNaN(value)), true);
await playback.prepare(100);
assert.equal(playback.residentChunkIndexes.length <= 3, true);
assert.equal(playback.residentChunkIndexes.includes(3), true);
assert.equal(playback.residentChunkIndexes.includes(4), true);
const selected = await playback.frame(100);
assert.deepEqual([...selected.states], [1, 1, 1, 1]);
const maximumResidentBytes = manifest.playback.centers.byte_length
+ manifest.playback.frames.byte_length
+ 3 * Math.max(...manifest.playback.chunks.map((value) => value.byte_length));
assert.equal(playback.residentByteLength <= maximumResidentBytes, true);
await playback.prepare(0);
assert.equal(playback.residentChunkIndexes.length <= 3, true);
assert.equal(playback.residentChunkIndexes.includes(0), true);
assert.equal(playback.residentChunkIndexes.includes(1), true);
assert.equal(
requested.every((url) => url.startsWith("/api/v1/laboratory/m49/physical-safety-playback")),
true,
);
});
test("physical-safety playback rejects a manifest that points at Worker", async () => {
const { manifest } = fixture();
manifest.playback.centers.url = "http://worker-006/centers";
await assert.rejects(
M49PhysicalSafetyPlaybackBuffer.open(resultId, {
fetcher: async () => Response.json(manifest),
}),
/sealed local API/,
);
});
@@ -74,24 +74,53 @@ test("M4.9T5 chunk keeps every missing-LiDAR cell explicitly UNOBSERVED", async
assert.deepEqual(new Set(chunk.frames[0].costmap.states), new Set([0]));
});
test("M4.9T5 viewer preloads one immutable binary playback pack", async () => {
const [source, contract] = await Promise.all([
test("M4.9T5 viewer prefers autonomous chunks and keeps a sealed legacy fallback", async () => {
const [source, visual, scene, contract] = await Promise.all([
readFile(
new URL("../src/workspaces/laboratory/M49TgsFullShadowEvidence.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/workspaces/laboratory/M4ReplayThreatVisual.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/components/laboratory/LaboratoryMetricEvidenceScene.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../src/core/laboratory/m49TgsFullShadow.ts", import.meta.url),
"utf8",
),
]);
assert.match(source, /fetchM49PhysicalSafetyPlaybackIdForSource/);
assert.match(source, /M49PhysicalSafetyPlaybackBuffer\.open/);
assert.match(source, /physicalPlayback\.frameIfResident/);
assert.match(source, /physicalPlayback\.prepare/);
assert.match(source, /fetchM49TgsFullShadowPlaybackPack/);
assert.match(source, /playbackProgress/);
assert.doesNotMatch(source, /fetchM49TgsFullShadowSpatialChunk/);
assert.match(source, /sampleAvailable: spatial\.sampleAvailable/);
assert.match(source, /sampleAvailable: frame\.sampleAvailable/);
assert.match(source, /packedCellsMapGravityLocal/);
assert.match(source, /playbackPack\.states\.subarray/);
assert.match(source, /playbackPack\.zBoundsM\.subarray/);
assert.doesNotMatch(source, /Array\.from\(\s*playbackPack\.states/);
assert.doesNotMatch(source, /centersXyM\.map\(/);
assert.match(source, /fetchE47SemanticSlamResult/);
assert.match(source, /next\.baseM4ResultId !== result\.source\.linkedVisualResultId/);
assert.match(source, /semantic=\{semantic \? \{/);
assert.match(
visual,
/classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/,
);
assert.doesNotMatch(visual, /classifiedSpatialFrame\?\.sampleAvailable !== false/);
assert.match(visual, /timelineFrame\.availableFrames\.find/);
assert.match(visual, /<LaboratoryMetricEvidenceScene/);
assert.doesNotMatch(
visual,
/\{\(!classifiedSpatialLayer \? spatialFrame : displayedClassifiedSpatialFrame\) \? \(/,
);
assert.match(scene, /useEffect\(\(\) => resetView\(\), \[mode, resetView\]\)/);
assert.match(contract, /linked_semantic_result_id/);
assert.match(contract, /missioncore\.m49-tgs-full-shadow-playback\/v1/);
});
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
@@ -9,6 +10,8 @@ let fetchM4ThreatReplayResult;
let fetchM4ThreatVisual;
let fetchM4ThreatTimeline;
let fetchM4ThreatTimelineChunk;
let fetchM4ThreatPlaybackManifest;
let fetchM4ThreatPlaybackPointChunk;
let fetchM4ThreatCameraPointOverlay;
let hydrateM4ThreatTimelineFrame;
let selectM4ThreatTimelineFrame;
@@ -33,6 +36,8 @@ before(async () => {
fetchM4ThreatVisual,
fetchM4ThreatTimeline,
fetchM4ThreatTimelineChunk,
fetchM4ThreatPlaybackManifest,
fetchM4ThreatPlaybackPointChunk,
fetchM4ThreatCameraPointOverlay,
hydrateM4ThreatTimelineFrame,
selectM4ThreatTimelineFrame,
@@ -373,6 +378,76 @@ test("M4.6 hydrates a lightweight timeline frame from one retained binary point
assert.equal(typeof hydrateM4ThreatTimelineFrame, "function");
});
test("M4.6 source cloud opens from one verified bounded chunk instead of the 105 MiB track", async () => {
const pointOffsets = [0, ...Array(4489).fill(2)];
const points = new Float32Array([11, 20, 30.25, 12, 19.5, 30]);
const pointBytes = points.buffer;
const pointSha256 = createHash("sha256").update(Buffer.from(pointBytes)).digest("hex");
const endpointRoot = "/api/v1/laboratory/m4-threat/results";
const chunks = Array.from({ length: 188 }, (_, index) => {
const start = index * 24;
const count = Math.min(24, 4489 - start);
const pointStart = pointOffsets[start];
const pointStop = pointOffsets[start + count];
const pointCount = pointStop - pointStart;
return {
index,
start,
count,
point_start: pointStart,
point_count: pointCount,
url: `${endpointRoot}/${resultId}/timeline/playback/chunks/${index}`,
media_type: "application/octet-stream",
dtype: "<f4",
shape: [pointCount, 3],
bytes: pointCount * 3 * 4,
sha256: index === 0 ? pointSha256 : "0".repeat(64),
};
});
const manifestPayload = {
schema_version: "missioncore.recorded-spatial-playback/v1",
result_id: resultId,
frame_count: 4489,
point_count: 2,
point_offsets: pointOffsets,
chunk_frame_count: 24,
resident_chunk_count_max: 4,
forward_prefetch_chunk_count: 1,
chunks,
track: {
id: "points-map-f32",
url: `${endpointRoot}/${resultId}/timeline/playback/tracks/points-map-f32`,
media_type: "application/octet-stream",
dtype: "<f4",
shape: [2, 3],
bytes: pointBytes.byteLength,
sha256: pointSha256,
},
coordinate_frame: "map",
access: "read-only-sealed-binary-playback",
};
const requested = [];
const fetcher = async (input) => {
const url = String(input);
requested.push(url);
if (url.endsWith("/timeline/playback")) return Response.json(manifestPayload);
if (url.endsWith("/timeline/playback/chunks/0")) return new Response(pointBytes.slice(0));
return new Response(null, { status: 404 });
};
const manifest = await fetchM4ThreatPlaybackManifest(resultId, { fetcher });
const chunk = await fetchM4ThreatPlaybackPointChunk(manifest, 0, { fetcher });
assert.deepEqual(requested, [
`${endpointRoot}/${resultId}/timeline/playback`,
`${endpointRoot}/${resultId}/timeline/playback/chunks/0`,
]);
assert.equal(chunk.pointCount, 2);
assert.equal(chunk.pointStart, 0);
assert.equal(chunk.sequenceCount, 24);
assert.equal(chunk.pointsMapXyzM.byteLength, 24);
assert.equal(requested.some((url) => url.endsWith("points-map-f32")), false);
});
test("M4.8S timeline binds factory-KB4 camera points through its exact endpoint", async () => {
const replayResultId = `m48s-fixed-class-detector-lab-${"b".repeat(64)}`;
const endpointRoot = "/api/v1/laboratory/m48s/fixed-class-detector";
@@ -825,10 +900,15 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visualCss, /width: 33\.333333%/);
assert.match(visualCss, /flex-flow: column nowrap/);
assert.match(visualCss, /m4-replay-threat-visual__overlay > div/);
assert.match(visualCss, /grid-template-columns: minmax\(0, max-content\)/);
assert.match(visualCss, /width: max-content/);
assert.match(visualCss, /grid-template-columns: minmax\(0, 1fr\)/);
assert.match(visualCss, /width: min\(17\.5rem/);
assert.match(visualCss, /grid-auto-rows: 3\.8rem/);
assert.match(visualCss, /--m4-replay-threat-overlay-pane-width/);
assert.match(visualCss, /white-space: nowrap/);
assert.match(visualCss, /-webkit-line-clamp: 2/);
assert.match(visual, /classifiedCellCount/);
assert.match(visual, /lastAvailableClassifiedSpatialFrame/);
assert.match(visual, /current UNOBSERVED/);
assert.match(visualCss, /laboratory-metric-evidence-scene__legend/);
assert.match(visualCss, /bottom: auto/);
assert.match(videoScene, /<RecordedFmp4Player/);
@@ -842,16 +922,18 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(visual, /showLocalSurface/);
assert.match(
visual,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: activeSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
/pointCloudBodyXyzM=\{displayedClassifiedSpatialFrame && replaceClassifiedPointCloud[\s\S]*\? classifiedPointsBody[\s\S]*: classifiedContextSpatialFrame\?\.pointCloudBodyXyzM \?\? \[\]\}/,
);
assert.match(
visual,
/const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame[\s\S]*lastClassifiedSpatialFrameRef/,
/const classifiedSpatialFrame = classifiedSpatialLayer\?\.frame\?\.sourceSequence === timelineFrame\.activeSequence[\s\S]*lastClassifiedSpatialFrameRef[\s\S]*const displayedClassifiedSpatialFrame = classifiedSpatialFrame\s*&&\s*classifiedSpatialFrame\.sampleAvailable !== false/,
);
assert.doesNotMatch(visual, /classifiedSpatialFrame\?\.sampleAvailable !== false/);
assert.match(visual, /timelineFrame\.availableFrames\.find/);
assert.match(visual, /localSurfaceBodyXyzM=\{localSurface\.pointsBodyXyzM\}/);
assert.match(visual, /showLocalSurface=\{showLocalSurface\}/);
assert.match(visual, /\{semantic \? \([\s\S]*>\s*SEMANTICS\s*<\/Button>/);
assert.match(visual, /все 2 244 TGS-ячейки явно UNOBSERVED/);
assert.match(visual, /current safety — все \{classifiedCellCount\.toLocaleString\("ru-RU"\)\} TGS-ячейки UNOBSERVED/);
assert.match(
visual,
/mapGravityLocalSensorToBodyGround[\s\S]*rotated\[2\] \+ nominalSensorHeightM/,
@@ -173,6 +173,9 @@ test("recorded player keeps full-archive range fallback and uses bounded generat
assert.match(source, /pumpRecordedSegmentWindow/);
assert.match(source, /waitForRecordedVideoTarget/);
assert.match(source, /removeRecordedMediaRange/);
assert.match(source, /hasPresentedFrame/);
assert.match(source, /retainForwardFrame/);
assert.match(source, /candidateTarget\.sequence >= previousTarget\.sequence/);
assert.equal(recordedMediaDecodeStartSequence([1, 1491, 1501], 1500), 1491);
assert.deepEqual(
@@ -57,7 +57,10 @@ test("metric evidence keeps missing semantic assignments as context and exposes
assert.match(source, /recordedEvidenceSemanticCssColor/);
assert.match(source, /DynamicDrawUsage/);
assert.match(source, /classifiedMeshesRef/);
assert.match(source, /classifiedPackedCells/);
assert.match(source, /updatePointPositions/);
assert.match(source, /renderer\.info\.render\.calls/);
assert.match(source, /data-decision="performance"/);
});
test("semantic point alignment follows the last qualified spatial increment", async () => {
@@ -0,0 +1,107 @@
{
"schema_version": "missioncore.m49-physical-safety-shadow-profile/v1",
"profile_id": "m49-mission-core-rover-physical-safety-shadow/v0",
"status": "planning-only-unqualified",
"vehicle": {
"source": "operator-supplied-2026-08-27-unverified",
"body_length_m": 1.0,
"body_width_m": 0.8,
"body_height_m": 0.4,
"ground_clearance_m": 0.15,
"nominal_mass_kg": 100.0,
"mass_qualification": "estimate-not-weighed",
"provisional_centered_footprint_body_xy_m": [
[0.5, 0.4],
[0.5, -0.4],
[-0.5, -0.4],
[-0.5, 0.4]
],
"nominal_speed_mps": 0.2777777778,
"maximum_operating_speed_mps": 0.5555555556,
"drive": {
"motor_count": 2,
"rated_power_per_motor_w": 500.0,
"motor_per_side": true,
"steering_model": "unqualified-likely-differential"
}
},
"unresolved_physical_inputs": [
"measured-mass-and-center-of-gravity",
"wheel-diameter-and-width",
"wheelbase-track-and-overhangs",
"qualified-base-link-origin",
"controller-braking-and-loss-of-power-behavior",
"measured-stopping-envelope",
"maximum-step-slope-and-side-slope",
"lidar-camera-to-body-extrinsics",
"negative-obstacle-sensor-coverage"
],
"terrain_evidence": {
"accepted_baseline_profile": "m49-tgs-full-shadow-v1.json",
"accepted_baseline_cell_size_m": 0.45,
"baseline_mutation_allowed": false,
"challenger_cell_sizes_m": [0.15, 0.1],
"challenger_radius_m": 12.0,
"missing_support_policy": "stop-never-free",
"required_states": [
"SUPPORTED",
"RIGID_OR_UNKNOWN_OBSTACLE",
"NEGATIVE_OR_UNSUPPORTED",
"VEGETATION_UNKNOWN",
"VEGETATION_POTENTIALLY_TRAVERSABLE",
"VEGETATION_WITH_RIGID_GEOMETRY"
]
},
"operator_visual": {
"host_profile": "14-inch-2023-macbook-pro-18-gib",
"worker_role": "realtime-only",
"lab_artifact_generation": "local-sequential-offline-or-imported-sealed",
"lab_build_may_take_longer_than_source_duration": true,
"source_pace_hz": 10.0,
"render_policy": "on-demand-with-interaction-damping",
"transport": "sealed-local-binary-chunks-no-per-cell-json",
"worker_runtime_dependency": false,
"sealed_artifact_required": true,
"offline_worker_open_required": true,
"full_session_high_resolution_preload_allowed": false,
"playback_buffer": {
"chunk_frame_count": 32,
"startup_prebuffer_chunk_count": 2,
"startup_prebuffer_seconds": 6.4,
"resident_chunk_count_max": 3,
"resident_window_seconds_max": 9.6,
"forward_prefetch_chunk_count": 1,
"backward_seek_policy": "load-target-and-next-from-sealed-local-artifact"
},
"lod": {
"safety_computation_reduced_by_visual_lod": false,
"near_field_radius_m": 4.0,
"near_field_cell_size_m": 0.15,
"mid_field_radius_m": 8.0,
"mid_field_cell_size_m": 0.3,
"far_field_radius_m": 12.0,
"far_field_cell_size_m": 0.45,
"paused_active_frame_full_resolution_available": true
},
"acceptance_budget": {
"main_thread_frame_prepare_p95_ms_max": 10.0,
"webgl_render_submit_p95_ms_target": 16.7,
"webgl_render_submit_p95_ms_max": 33.3,
"evidence_to_visible_p95_ms_max": 100.0,
"incremental_resident_memory_mib_max": 256.0,
"post_warmup_memory_growth_mib_max": 32.0,
"long_task_over_50ms_count_max": 0,
"webgl_context_loss_count_max": 0,
"worker_unavailable_open_failure_count_max": 0,
"worker_request_count_during_lab_open_max": 0,
"worker_request_count_during_lab_build_max": 0
}
},
"authority": {
"commands_enabled": false,
"navigation_accepted": false,
"safety_accepted": false,
"actuation_accepted": false,
"physical_test_accepted": false
}
}
@@ -0,0 +1,186 @@
# ADR 0043: Physical safety shadow and bounded operator visual
Date: 2026-08-27
Status: autonomous transport slice implemented; 0.15 m challenger and measured acceptance incomplete
## Context
Mission Core has accepted a source-paced RF-DETR-native plus TRAVEL TGS
recorded shadow on RAVNOVES00. The retained TGS result uses a `0.45 m` cell,
`12 m` radius and four explicit states. It is useful operator evidence but is
not a planner-authoritative terrain provider. The integrated acceptance joins
TGS and the reference graph as evidence; TGS does not modify graph state.
The intended physical rover is not a lightweight laboratory fixture. The
operator supplied the following planning facts, not yet independently measured:
- body `1.0 x 0.8 x 0.4 m`;
- ground clearance `0.15 m`;
- mass approximately `100 kg`;
- one nominal `500 W` motor per side;
- nominal speed `0.2778 m/s` (`1 km/h`);
- maximum operating speed `0.5556 m/s` (`2 km/h`).
Controller, remote dead-man, braking behavior, physical E-stop, body/sensor
extrinsics, center of gravity and negative-obstacle coverage are unavailable.
Commands therefore remain disabled.
The current full TGS playback pack is about `88 MiB`: approximately `9.6 MiB`
states, `77 MiB` z-bounds and `1.1 MiB` frame metadata. Holding the same
`12 m` disk for all `4,489` frames would scale to roughly `0.8 GiB` at `0.15 m`
and `1.8 GiB` at `0.10 m`, before richer terrain features, video, semantics and
renderer allocations. Full-session high-resolution browser preload is not an
admitted design for the current 18 GiB MacBook host.
## Decision
Recorded LAB and realtime perception are different runtime contours. Worker
006 is reserved for realtime perception. A new recorded LAB is built by a
bounded sequential offline builder on the MacBook, or admits an already sealed
immutable artifact. The offline build may take longer than source duration. It
must not become a hidden Worker job. An opened LAB never depends on Worker 006.
```text
recorded LAB publication, Worker 006 absent
recorded source -> local sequential offline build
-> seal immutable result + binary LOD tracks
|
v
local/CAS artifact store
|
Worker 006 remains absent
|
v
canonical local backend, read-only
|
v
MacBook LAB CAMERA + 3D + PLAN
separate future realtime contour
K1 -> Worker 006 -> live world-state/planner shadow
```
Opening, seeking, looping and visually inspecting a sealed LAB must perform
zero requests to Worker 006. If the worker, its host, Docker or the network path
to it is unavailable, a locally present sealed result opens unchanged. If a
manifest-declared local object is absent or corrupt, LAB fails with an explicit
artifact error; it does not start compute, scan worker results or silently
rebuild evidence. This preserves ADR 0033.
Publishing a new LAB also performs zero Worker requests. Slow local preparation
is acceptable, but it is explicit and bounded: build first, seal second, open
third. Historical LAB artifacts that were originally computed on Worker 006
remain valid after sealing because replay depends only on their immutable
artifacts; they do not justify a Worker dependency for new LABs.
Visual performance may never change a safety state, erase unknown space or
reduce the resolution used by the safety consumer. Conversely, the MacBook does
not need every cell of every high-resolution frame resident at once in order to
provide a complete operator explanation.
The retained `0.45 m` M49T5 profile and sealed result remain immutable. A new
physical-safety challenger may evaluate `0.15 m` and `0.10 m` cells without
rewriting that baseline. The first candidate resolution is `0.15 m`; `0.10 m`
is admitted only if LiDAR density improves the evidence. Worker compute
acceptance and autonomous LAB viewer acceptance are measured separately.
The operator transport uses binary typed arrays. It must not serialize one JSON
object per cell or expand binary frames into nested JavaScript objects before
updating GPU instances. Recorded high-resolution inspection reads only sealed
local binary chunks and active-frame detail, not a full-session high-resolution
preload.
The proposed spatial chunk is `32` source frames (`3.2 s` at `10 Hz`). Before
the spatial scene declares itself ready, it may preload the first two chunks,
or `6.4 s`, from the local artifact. The browser retains at most three chunks
(`9.6 s`), normally previous/current/next, and prefetches one forward chunk.
A backward seek loads the target and next chunk from the same sealed artifact.
This bounded startup delay is preferable to either Worker dependency or a
multi-gigabyte browser preload.
## Implemented autonomous transport slice
The first executable transport slice is now present without creating a new LAB
surface or transport clock:
- `m49_physical_safety_playback.py` is a local, sequential, filesystem-only
sealer. It accepts an already sealed source, verifies its declared hashes,
and writes immutable binary chunks atomically. It contains no Worker client,
job submission or network path.
- `m49_physical_safety_playback_api.py` exposes only the configured local
artifact root. Manifest, shared centers, frame catalog and every requested
chunk are hash-verified. Corruption returns an explicit artifact failure.
- `m49PhysicalSafetyPlayback.ts` rejects non-canonical URLs, verifies byte
length and SHA-256 in the browser, prebuffers two chunks, retains no more than
three and loads target plus next on seek.
- the existing M49 evidence composition resolves this transport by exact
`source_result_id`. If no chunked derivative has been published, it may still
read the old sealed local full pack; neither path uses Worker 006.
A real transport canary was generated from the accepted immutable `0.45 m`
M49T5 result without recomputing TGS or changing that source result. The canary
identity is
`m49-physical-safety-playback-0d0e41a995a902a4701935a79655d93f2f1898806f8fa82ebbd207129f111d69`.
It contains all `4,489` frames and `2,244` cells in `141` chunks. A normal
32-frame chunk is `646,300` bytes; two startup chunks are about `1.23 MiB` and
three spatial chunks are about `1.85 MiB`, plus the shared centers and frame
catalog. This is the transport canary only. It is not a `0.15 m` safety result
and no upsampled baseline is presented as new evidence.
The `0.15 m` shape, fail-closed missing-sample rule, bounded seek behavior and
zero-Worker URL policy are covered with synthetic contract fixtures. A real
`0.15 m` challenger still requires the local sequential terrain computation
and a separately sealed result.
The visual uses explicit LOD:
- `0-4 m`: `0.15 m` near-field cells;
- `4-8 m`: `0.30 m` explanatory cells;
- `8-12 m`: `0.45 m` context cells;
- pause/anchor inspection: full available active-frame resolution.
LOD changes only presentation. The sealed result retains the full candidate
evidence needed to reproduce its decisions; a future realtime worker separately
retains the full live safety grid across the configured safety radius.
The metric Three.js scene retains instanced meshes and all evidence layers. It
renders on evidence, camera interaction, resize or layer changes. It does not
run a permanent animation loop while idle. The scene exposes render-pass time,
draw calls, triangles, cell count and pixel ratio in the operator surface.
## Acceptance budget
Acceptance is measured on the named 14-inch 2023 MacBook Pro with 18 GiB RAM,
with recorded video, semantic overlay and TGS 3D/PLAN evidence available:
- source pace: `10 Hz` without evidence loss;
- main-thread frame preparation p95 at most `10 ms`;
- CPU-side WebGL render submission p95 target `16.7 ms`, hard maximum
`33.3 ms`; GPU completion requires a separate supported timer query;
- evidence-to-visible p95 at most `100 ms`;
- incremental visual resident memory at most `256 MiB`;
- post-warmup growth at most `32 MiB` over a ten-minute soak;
- zero browser long tasks above `50 ms` during source-paced playback;
- zero WebGL context losses;
- zero Worker 006 requests while opening, seeking or playing a LAB;
- zero Worker 006 requests while building a new recorded LAB;
- the sealed LAB opens with Worker 006 unavailable;
- idle metric scene performs no continuous render loop.
These are proposed thresholds until a canonical browser run emits a sealed
performance ledger. Passing them does not grant navigation, safety or actuation
authority.
## Consequences
- The operator keeps the full explanatory visual instead of receiving a
stripped-down safety UI.
- High-resolution terrain work does not turn the MacBook into a second
perception worker.
- Historical LAB remains usable during Worker maintenance, Docker failure or
loss of the Worker network path.
- Evidence identity and safety decisions remain independent of display LOD.
- The next implementation boundary is a packed, chunked terrain contract plus
footprint-aware states; no physical connection or command path is introduced.
- Physical authority remains blocked by controller/E-stop, measured braking,
vehicle geometry, sensor extrinsics and negative-obstacle coverage.
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""Seal a local M49 playback without contacting Worker 006."""
from __future__ import annotations
import argparse
from pathlib import Path
from k1link.laboratory.m49_physical_safety_playback import (
seal_m49_physical_safety_playback,
)
def main() -> None:
parser = argparse.ArgumentParser(
description="Build an autonomous sealed M49 physical-safety playback artifact."
)
parser.add_argument("--source-root", type=Path, required=True)
parser.add_argument("--destination-root", type=Path, required=True)
parser.add_argument(
"--profile",
type=Path,
default=Path("config/perception/m49-physical-safety-shadow-v0.json"),
)
arguments = parser.parse_args()
result = seal_m49_physical_safety_playback(
source_root=arguments.source_root,
destination_root=arguments.destination_root,
profile_path=arguments.profile,
)
print(result.result_id)
if __name__ == "__main__":
main()
@@ -0,0 +1,538 @@
"""Build and verify autonomous chunked M49 physical-safety playback artifacts."""
from __future__ import annotations
import hashlib
import json
import math
import shutil
import struct
import tempfile
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final
import numpy as np
SCHEMA: Final = "missioncore.m49-physical-safety-playback/v1"
PROFILE_SCHEMA: Final = "missioncore.m49-physical-safety-shadow-profile/v1"
PREFIX: Final = "m49-physical-safety-playback-"
CHUNK_MAGIC: Final = b"MCPSCH01"
CHUNK_HEADER: Final = struct.Struct("<8sIIIII")
SOURCE_TRACKS: Final = (
"costmap-cell-centers-xy-m.npy",
"costmap-states.npy",
"costmap-z-bounds-m.npy",
"frames.ndjson",
)
_HASH_BLOCK_BYTES: Final = 1024 * 1024
_MAX_JSON_BYTES: Final = 4 * 1024 * 1024
class M49PhysicalSafetyPlaybackError(RuntimeError):
"""The autonomous playback artifact is missing or violates its contract."""
@dataclass(frozen=True, slots=True)
class M49PhysicalSafetyPlayback:
result_id: str
root: Path
manifest: dict[str, Any]
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(_HASH_BLOCK_BYTES), b""):
digest.update(block)
return digest.hexdigest()
def _canonical_sha256(value: object) -> str:
content = json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(content).hexdigest()
def _json(path: Path, label: str) -> dict[str, Any]:
if path.is_symlink() or not path.is_file() or path.stat().st_size > _MAX_JSON_BYTES:
raise M49PhysicalSafetyPlaybackError(f"{label} is unavailable")
try:
value = json.loads(path.read_text(encoding="utf-8-sig"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
raise M49PhysicalSafetyPlaybackError(f"{label} is invalid") from error
if not isinstance(value, dict):
raise M49PhysicalSafetyPlaybackError(f"{label} is invalid")
return value
def _artifact(path: Path, *, media_type: str, role: str) -> dict[str, object]:
return {
"path": path.name,
"media_type": media_type,
"role": role,
"byte_length": path.stat().st_size,
"sha256": _sha256(path),
}
def _source_artifacts(source: Path, manifest: dict[str, Any]) -> dict[str, dict[str, Any]]:
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list):
raise M49PhysicalSafetyPlaybackError("source artifact catalog is unavailable")
result: dict[str, dict[str, Any]] = {}
for value in artifacts:
if not isinstance(value, dict) or not isinstance(value.get("path"), str):
raise M49PhysicalSafetyPlaybackError("source artifact catalog is invalid")
name = value["path"]
if name in (*SOURCE_TRACKS, "report.json"):
path = source / name
if (
path.parent != source
or path.is_symlink()
or not path.is_file()
or value.get("byte_length") != path.stat().st_size
or value.get("sha256") != _sha256(path)
):
raise M49PhysicalSafetyPlaybackError(f"source artifact changed: {name}")
result[name] = value
if set(result) != {*SOURCE_TRACKS, "report.json"}:
raise M49PhysicalSafetyPlaybackError("source spatial tracks are incomplete")
return result
def _state_codes(report: dict[str, Any]) -> dict[str, int]:
candidates = (
report.get("state_codes"),
report.get("visual_review", {}).get("state_codes")
if isinstance(report.get("visual_review"), dict)
else None,
)
for candidate in candidates:
if not isinstance(candidate, dict):
continue
parsed = {
str(name): int(code)
for name, code in candidate.items()
if isinstance(name, str)
and isinstance(code, int)
and not isinstance(code, bool)
and 0 <= code <= 255
}
if len(parsed) == len(candidate) and parsed.get("UNOBSERVED") == 0:
return parsed
raise M49PhysicalSafetyPlaybackError("source state codes are unavailable")
def _frame_rows(path: Path) -> tuple[dict[str, Any], ...]:
rows: list[dict[str, Any]] = []
try:
with path.open("r", encoding="utf-8") as stream:
for sequence, line in enumerate(stream):
value = json.loads(line)
if (
not isinstance(value, dict)
or not isinstance(value.get("sample_available"), bool)
or not isinstance(value.get("source_frame_index"), int)
or isinstance(value.get("source_frame_index"), bool)
or not isinstance(value.get("session_seconds"), (int, float))
or isinstance(value.get("session_seconds"), bool)
or not math.isfinite(float(value["session_seconds"]))
):
raise M49PhysicalSafetyPlaybackError(
f"source frame catalog changed at sequence {sequence}"
)
rows.append(value)
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
raise M49PhysicalSafetyPlaybackError("source frame catalog is invalid") from error
if not rows:
raise M49PhysicalSafetyPlaybackError("source frame catalog is empty")
return tuple(rows)
def _source_geometry(report: dict[str, Any]) -> tuple[float, float, str]:
configuration = report.get("configuration")
if not isinstance(configuration, dict):
raise M49PhysicalSafetyPlaybackError("source geometry is unavailable")
cell_size = configuration.get("cell_size_m")
radius = configuration.get("radius_m")
coordinate_frame = configuration.get("coordinate_frame")
if (
not isinstance(cell_size, (int, float))
or isinstance(cell_size, bool)
or not math.isfinite(float(cell_size))
or float(cell_size) <= 0
or not isinstance(radius, (int, float))
or isinstance(radius, bool)
or not math.isfinite(float(radius))
or float(radius) <= 0
or coordinate_frame != "map-gravity-local"
):
raise M49PhysicalSafetyPlaybackError("source geometry changed")
return float(cell_size), float(radius), coordinate_frame
def _profile_policy(profile: dict[str, Any], cell_size_m: float) -> dict[str, int]:
visual = profile.get("operator_visual")
terrain = profile.get("terrain_evidence")
if (
profile.get("schema_version") != PROFILE_SCHEMA
or not isinstance(visual, dict)
or not isinstance(terrain, dict)
or visual.get("worker_role") != "realtime-only"
or visual.get("worker_runtime_dependency") is not False
or visual.get("sealed_artifact_required") is not True
):
raise M49PhysicalSafetyPlaybackError("physical-safety profile changed")
playback = visual.get("playback_buffer")
admitted_sizes = [
terrain.get("accepted_baseline_cell_size_m"),
*(terrain.get("challenger_cell_sizes_m") or []),
]
if (
not isinstance(playback, dict)
or playback.get("chunk_frame_count") != 32
or playback.get("startup_prebuffer_chunk_count") != 2
or playback.get("resident_chunk_count_max") != 3
or not any(
isinstance(value, (int, float))
and not isinstance(value, bool)
and math.isclose(float(value), cell_size_m, abs_tol=1e-9)
for value in admitted_sizes
)
):
raise M49PhysicalSafetyPlaybackError("physical-safety playback policy changed")
return {
"chunk_frame_count": 32,
"startup_prebuffer_chunk_count": 2,
"resident_chunk_count_max": 3,
"forward_prefetch_chunk_count": 1,
}
def _chunk_payload(
path: Path,
*,
start: int,
states: np.ndarray,
z_bounds: np.ndarray,
) -> None:
frame_count, cell_count = states.shape
state_values = np.ascontiguousarray(states, dtype=np.uint8)
z_values = np.ascontiguousarray(z_bounds, dtype="<f4")
state_bytes = state_values.nbytes
z_bytes = z_values.nbytes
padding = (4 - ((CHUNK_HEADER.size + state_bytes) % 4)) % 4
with path.open("wb") as stream:
stream.write(
CHUNK_HEADER.pack(
CHUNK_MAGIC,
start,
frame_count,
cell_count,
state_bytes,
z_bytes,
)
)
stream.write(memoryview(state_values).cast("B"))
if padding:
stream.write(b"\0" * padding)
stream.write(memoryview(z_values).cast("B"))
def seal_m49_physical_safety_playback(
*,
source_root: Path,
destination_root: Path,
profile_path: Path,
created_at_utc: str | None = None,
) -> M49PhysicalSafetyPlayback:
"""Sequentially seal local tracks without starting or contacting Worker 006."""
raw_source = source_root.expanduser().absolute()
if raw_source.is_symlink() or not raw_source.is_dir():
raise M49PhysicalSafetyPlaybackError("sealed source root is unavailable")
source = raw_source.resolve(strict=True)
destination = destination_root.expanduser().absolute()
destination.mkdir(parents=True, exist_ok=True)
if destination.is_symlink():
raise M49PhysicalSafetyPlaybackError("destination must not be a symlink")
raw_profile = profile_path.expanduser().absolute()
if raw_profile.is_symlink() or not raw_profile.is_file():
raise M49PhysicalSafetyPlaybackError("physical-safety profile is unavailable")
profile_file = raw_profile.resolve(strict=True)
source_manifest = _json(source / "manifest.json", "source manifest")
source_report = _json(source / "report.json", "source report")
source_result_id = source_manifest.get("result_id")
source_identity = source_manifest.get("identity")
source_identity_sha256 = source_manifest.get("identity_sha256")
if (
not isinstance(source_result_id, str)
or source_report.get("result_id") != source_result_id
or not isinstance(source_identity, dict)
or not isinstance(source_identity_sha256, str)
or source_identity_sha256 != _canonical_sha256(source_identity)
or not source_result_id.endswith(source_identity_sha256)
):
raise M49PhysicalSafetyPlaybackError("source result identity changed")
source_artifacts = _source_artifacts(source, source_manifest)
state_codes = _state_codes(source_report)
cell_size_m, radius_m, coordinate_frame = _source_geometry(source_report)
profile = _json(profile_file, "physical-safety profile")
policy = _profile_policy(profile, cell_size_m)
profile_sha256 = _sha256(profile_file)
source_manifest_sha256 = _sha256(source / "manifest.json")
try:
centers = np.load(
source / "costmap-cell-centers-xy-m.npy", mmap_mode="r", allow_pickle=False
)
states = np.load(source / "costmap-states.npy", mmap_mode="r", allow_pickle=False)
z_bounds = np.load(
source / "costmap-z-bounds-m.npy", mmap_mode="r", allow_pickle=False
)
except (OSError, ValueError) as error:
raise M49PhysicalSafetyPlaybackError("source numeric tracks are invalid") from error
frames = _frame_rows(source / "frames.ndjson")
if (
centers.dtype != np.dtype("<f4")
or states.dtype != np.dtype("uint8")
or z_bounds.dtype != np.dtype("<f4")
or centers.ndim != 2
or centers.shape[1] != 2
or states.ndim != 2
or z_bounds.ndim != 3
or z_bounds.shape[2] != 2
or states.shape[0] != len(frames)
or states.shape[1] != centers.shape[0]
or z_bounds.shape != (states.shape[0], states.shape[1], 2)
or not np.isfinite(centers).all()
):
raise M49PhysicalSafetyPlaybackError("source numeric shape or dtype changed")
frame_count, cell_count = states.shape
admitted_codes = np.asarray(sorted(set(state_codes.values())), dtype=np.uint8)
identity = {
"schema_version": SCHEMA,
"source_result_id": source_result_id,
"source_manifest_sha256": source_manifest_sha256,
"source_track_sha256": {
name: source_artifacts[name]["sha256"] for name in SOURCE_TRACKS
},
"profile_id": profile.get("profile_id"),
"profile_sha256": profile_sha256,
"coordinate_frame": coordinate_frame,
"frame_count": frame_count,
"cell_count": cell_count,
"cell_size_m": cell_size_m,
"radius_m": radius_m,
"state_codes": state_codes,
"playback_policy": policy,
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
"actuation_accepted": False,
},
}
identity_sha256 = _canonical_sha256(identity)
result_id = f"{PREFIX}{identity_sha256}"
target = destination / result_id
if target.exists():
existing = read_m49_physical_safety_playback(target)
existing_playback = existing.manifest["playback"]
assert isinstance(existing_playback, dict)
existing_chunks = existing_playback["chunks"]
assert isinstance(existing_chunks, list)
for value in (
existing_playback["centers"],
existing_playback["frames"],
*existing_chunks,
):
assert isinstance(value, dict)
verify_m49_physical_safety_artifact(existing, value)
return existing
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
chunk_frame_count = int(policy["chunk_frame_count"])
with tempfile.TemporaryDirectory(
prefix="mission-core-m49-physical-safety-", dir=destination
) as raw_staging:
staging = Path(raw_staging) / result_id
staging.mkdir()
centers_path = staging / "centers.f32"
np.ascontiguousarray(centers, dtype="<f4").tofile(centers_path)
frames_path = staging / "frames.ndjson"
shutil.copyfile(source / "frames.ndjson", frames_path)
centers_artifact = _artifact(
centers_path,
media_type="application/octet-stream",
role="costmap-cell-centers-f32le",
)
frames_artifact = _artifact(
frames_path,
media_type="application/x-ndjson",
role="frame-catalog",
)
chunks: list[dict[str, object]] = []
for index, start in enumerate(range(0, frame_count, chunk_frame_count)):
count = min(chunk_frame_count, frame_count - start)
state_slice = states[start : start + count]
if not np.isin(state_slice, admitted_codes).all():
raise M49PhysicalSafetyPlaybackError(
f"source state code changed in chunk {index}"
)
for local_index, frame in enumerate(frames[start : start + count]):
if frame["sample_available"] is False and (
np.any(state_slice[local_index] != 0)
or np.isfinite(z_bounds[start + local_index]).any()
):
raise M49PhysicalSafetyPlaybackError(
"missing source sample did not remain fail-closed"
)
chunk_path = staging / f"chunk-{index:06d}.bin"
_chunk_payload(
chunk_path,
start=start,
states=state_slice,
z_bounds=z_bounds[start : start + count],
)
descriptor = _artifact(
chunk_path,
media_type="application/octet-stream",
role="states-u8-and-z-bounds-f32le",
)
descriptor.update(
{
"index": index,
"start": start,
"count": count,
"header_bytes": CHUNK_HEADER.size,
"format": "mcpsch01-states-u8-aligned-z-bounds-f32le",
}
)
chunks.append(descriptor)
manifest = {
"schema_version": SCHEMA,
"result_id": result_id,
"created_at_utc": created,
"identity_sha256": identity_sha256,
"identity": identity,
"execution": {
"execution_class": "local-sequential-offline",
"worker_role": "realtime-only",
"worker_runtime_dependency": False,
"worker_requests_required": 0,
},
"playback": {
"coordinate_frame": coordinate_frame,
"source_pace_hz": profile["operator_visual"]["source_pace_hz"],
"frame_count": frame_count,
"cell_count": cell_count,
"cell_size_m": cell_size_m,
"radius_m": radius_m,
"state_codes": state_codes,
**policy,
"centers": {
**centers_artifact,
"dtype": "<f4",
"shape": [cell_count, 2],
},
"frames": {
**frames_artifact,
"dtype": "ndjson",
"shape": [frame_count],
},
"chunks": chunks,
},
"authority": identity["authority"],
}
(staging / "manifest.json").write_text(
json.dumps(manifest, indent=2, sort_keys=True, allow_nan=False) + "\n",
encoding="utf-8",
)
staging.replace(target)
return read_m49_physical_safety_playback(target)
def read_m49_physical_safety_playback(root: Path) -> M49PhysicalSafetyPlayback:
raw_candidate = root.expanduser().absolute()
if raw_candidate.is_symlink() or not raw_candidate.is_dir():
raise M49PhysicalSafetyPlaybackError("physical-safety playback root is invalid")
candidate = raw_candidate.resolve(strict=True)
manifest = _json(candidate / "manifest.json", "physical-safety playback manifest")
identity = manifest.get("identity")
execution = manifest.get("execution")
if (
manifest.get("schema_version") != SCHEMA
or not isinstance(identity, dict)
or not isinstance(execution, dict)
or manifest.get("identity_sha256") != _canonical_sha256(identity)
or manifest.get("result_id") != candidate.name
or candidate.name != f"{PREFIX}{manifest.get('identity_sha256')}"
or execution.get("worker_requests_required") != 0
or execution.get("worker_runtime_dependency") is not False
):
raise M49PhysicalSafetyPlaybackError("physical-safety playback identity changed")
playback = manifest.get("playback")
if not isinstance(playback, dict) or not isinstance(playback.get("chunks"), list):
raise M49PhysicalSafetyPlaybackError("physical-safety playback catalog changed")
descriptors = [playback.get("centers"), playback.get("frames"), *playback["chunks"]]
seen: set[str] = set()
for descriptor in descriptors:
if not isinstance(descriptor, dict) or not isinstance(descriptor.get("path"), str):
raise M49PhysicalSafetyPlaybackError("physical-safety artifact entry changed")
name = descriptor["path"]
path = candidate / name
if (
name in seen
or path.parent != candidate
or path.is_symlink()
or not path.is_file()
or descriptor.get("byte_length") != path.stat().st_size
or not isinstance(descriptor.get("sha256"), str)
or len(descriptor["sha256"]) != 64
or any(character not in "0123456789abcdef" for character in descriptor["sha256"])
):
raise M49PhysicalSafetyPlaybackError("physical-safety artifact changed")
seen.add(name)
return M49PhysicalSafetyPlayback(candidate.name, candidate, manifest)
def verify_m49_physical_safety_artifact(
result: M49PhysicalSafetyPlayback,
descriptor: dict[str, Any],
) -> Path:
name = descriptor.get("path")
if not isinstance(name, str):
raise M49PhysicalSafetyPlaybackError("physical-safety artifact path changed")
path = result.root / name
if (
path.parent != result.root
or path.is_symlink()
or not path.is_file()
or descriptor.get("byte_length") != path.stat().st_size
or descriptor.get("sha256") != _sha256(path)
):
raise M49PhysicalSafetyPlaybackError("physical-safety artifact digest changed")
return path
__all__ = [
"CHUNK_HEADER",
"CHUNK_MAGIC",
"M49PhysicalSafetyPlayback",
"M49PhysicalSafetyPlaybackError",
"PREFIX",
"SCHEMA",
"read_m49_physical_safety_playback",
"seal_m49_physical_safety_playback",
"verify_m49_physical_safety_artifact",
]
+35 -7
View File
@@ -452,6 +452,32 @@ def build_threat_replay(
def read_threat_replay_result(root: Path) -> ThreatReplayResult:
"""Read and deeply verify a sealed threat replay result.
This is the acceptance/build seam. It deliberately walks every ledger row
before returning and therefore must not sit on the synchronous LAB open
path for a hundreds-of-megabytes recorded result.
"""
return _read_threat_replay_result(root, validate_ledgers=True)
def read_threat_replay_result_metadata(root: Path) -> ThreatReplayResult:
"""Read verified result metadata without eagerly parsing the full ledger.
Artifact bytes are still digest checked against the sealed manifest. The
bounded timeline reader validates every requested row before projection;
only the redundant all-4489-row JSON accounting pass is deferred.
"""
return _read_threat_replay_result(root, validate_ledgers=False)
def _read_threat_replay_result(
root: Path,
*,
validate_ledgers: bool,
) -> ThreatReplayResult:
resolved = root.resolve(strict=True)
if resolved.is_symlink() or not resolved.name.startswith(THREAT_REPLAY_RESULT_PREFIX):
raise ThreatReplayError("threat replay result root is invalid")
@@ -498,7 +524,8 @@ def read_threat_replay_result(root: Path) -> ThreatReplayResult:
for role, (name, identity_key) in expected.items():
path = _validated_artifact(resolved, by_role[role], name)
paths[role] = path
if identity_key is not None and _file_sha256(path) != identity.get(identity_key):
artifact = _object(by_role[role], "threat artifact")
if identity_key is not None and artifact.get("sha256") != identity.get(identity_key):
raise ThreatReplayError("threat artifact identity changed")
report = _read_json(paths["threat-replay-report"])
metrics = _object(identity.get("metrics"), "threat metrics")
@@ -522,12 +549,13 @@ def read_threat_replay_result(root: Path) -> ThreatReplayResult:
or requirements != expected_requirements
):
raise ThreatReplayError("threat replay report or acceptance changed")
_validate_ledgers(
paths["threat-replay-frames"],
paths["threat-visual-frames"],
metrics,
is_v2=is_v2,
)
if validate_ledgers:
_validate_ledgers(
paths["threat-replay-frames"],
paths["threat-visual-frames"],
metrics,
is_v2=is_v2,
)
return ThreatReplayResult(
result_id=resolved.name,
result_root=resolved,
+125 -15
View File
@@ -13,6 +13,8 @@ from pathlib import Path
from threading import RLock
from typing import Final
import numpy as np
from .geometry import RecordedGeometryStore
from .recorded_source import RECORDED_REPRESENTATION_ID
from .spatial_evidence import (
@@ -78,24 +80,56 @@ class RecordedThreatTimeline:
}
if any(identity.get(key) != value for key, value in expected_identity.items()):
raise RecordedThreatTimelineError("recorded timeline escaped the threat profile")
self.store = RecordedGeometryStore.from_repository(self.repository_root)
if (
self.store.profile.source_pack_id != self.profile.source_pack_id
or self.store.profile.source_pack_sha256 != self.profile.source_pack_sha256
or self.store.profile.frame_count != _EXPECTED_FRAME_COUNT
):
raise RecordedThreatTimelineError("recorded timeline geometry identity changed")
if self.store.maximum_current_point_count > RECORDED_SPATIAL_POINT_LIMIT:
raise RecordedThreatTimelineError(
"recorded timeline exact point delivery exceeds its declared bound"
)
self.body_frames = RecordedReplayBodyFrameResolver(
self.store,
profile=self.profile.body_frame,
self._maximum_source_points_per_frame = _read_source_point_bound(
self.repository_root,
source_pack_id=self.profile.source_pack_id,
source_pack_sha256=self.profile.source_pack_sha256,
)
self._store: RecordedGeometryStore | None = None
self._body_frames: RecordedReplayBodyFrameResolver | None = None
self.index = _index_frame_ledger(self.frames_path)
self._lock = RLock()
@property
def store(self) -> RecordedGeometryStore:
"""Load and deeply verify the large geometry archives on first use.
Timeline metadata, camera playback and the independent TGS artifact can
become visible without waiting for the source cloud archive. Any
endpoint that actually delivers source points still crosses the full
digest and shape validation in ``RecordedGeometryStore``.
"""
with self._lock:
if self._store is None:
store = RecordedGeometryStore.from_repository(self.repository_root)
if (
store.profile.source_pack_id != self.profile.source_pack_id
or store.profile.source_pack_sha256 != self.profile.source_pack_sha256
or store.profile.frame_count != _EXPECTED_FRAME_COUNT
or store.maximum_current_point_count
!= self._maximum_source_points_per_frame
):
raise RecordedThreatTimelineError(
"recorded timeline geometry identity changed"
)
if store.maximum_current_point_count > RECORDED_SPATIAL_POINT_LIMIT:
raise RecordedThreatTimelineError(
"recorded timeline exact point delivery exceeds its declared bound"
)
self._store = store
return self._store
@property
def body_frames(self) -> RecordedReplayBodyFrameResolver:
with self._lock:
if self._body_frames is None:
self._body_frames = RecordedReplayBodyFrameResolver(
self.store,
profile=self.profile.body_frame,
)
return self._body_frames
def metadata(self) -> dict[str, object]:
times = self.index.source_times_ns
intervals = [(current - previous) / 1_000_000_000 for previous, current in pairwise(times)]
@@ -120,7 +154,7 @@ class RecordedThreatTimeline:
"max_chunk_frames": RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
"point_sample_limit": RECORDED_SPATIAL_POINT_LIMIT,
"point_delivery": "exact-current-increment",
"maximum_source_points_per_frame": self.store.maximum_current_point_count,
"maximum_source_points_per_frame": self._maximum_source_points_per_frame,
"local_surface_visualization": {
"derivation": "bounded-registered-increment-accumulation",
"window_seconds": RECORDED_LOCAL_SURFACE_WINDOW_SECONDS,
@@ -258,6 +292,82 @@ class RecordedThreatTimeline:
}
def _read_source_point_bound(
repository_root: Path,
*,
source_pack_id: str,
source_pack_sha256: str,
) -> int:
"""Read the small offsets member without inflating the 70 MiB source pack.
This is metadata only. The exact archive digest, every array shape and the
local-surface binding are still verified lazily by ``RecordedGeometryStore``
before any source point is delivered.
"""
base = (
repository_root / ".runtime/compute-experiments/e10/lidar-packs"
).resolve(strict=True)
pack_root = (base / source_pack_id).resolve(strict=True)
manifest_path = (pack_root / "manifest.json").resolve(strict=True)
pack_path = (pack_root / "lidar-pack.npz").resolve(strict=True)
if (
pack_root.parent != base
or pack_root.is_symlink()
or manifest_path.parent != pack_root
or manifest_path.is_symlink()
or pack_path.parent != pack_root
or pack_path.is_symlink()
or not pack_path.is_file()
):
raise RecordedThreatTimelineError("recorded timeline source pack path changed")
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
if not isinstance(manifest, dict):
raise RecordedThreatTimelineError(
"recorded timeline source pack manifest is invalid"
)
identity = manifest.get("identity")
artifact = manifest.get("artifact")
if not isinstance(identity, dict) or not isinstance(artifact, dict):
raise RecordedThreatTimelineError(
"recorded timeline source pack manifest is invalid"
)
point_count = identity.get("point_count")
if (
manifest.get("pack_id") != source_pack_id
or identity.get("frame_count") != _EXPECTED_FRAME_COUNT
or not isinstance(point_count, int)
or isinstance(point_count, bool)
or point_count < 0
or artifact.get("path") != "lidar-pack.npz"
or artifact.get("sha256") != source_pack_sha256
or artifact.get("byte_length") != pack_path.stat().st_size
):
raise RecordedThreatTimelineError(
"recorded timeline source pack identity changed"
)
with np.load(pack_path, allow_pickle=False) as archive:
offsets = np.asarray(archive["cloud_offsets"], dtype=np.int64)
except (KeyError, OSError, ValueError, json.JSONDecodeError) as error:
raise RecordedThreatTimelineError(
"recorded timeline source point offsets are unavailable"
) from error
if (
offsets.shape != (_EXPECTED_FRAME_COUNT + 1,)
or int(offsets[0]) != 0
or int(offsets[-1]) != point_count
or np.any(np.diff(offsets) < 0)
):
raise RecordedThreatTimelineError("recorded timeline source point offsets changed")
maximum = int(np.diff(offsets).max(initial=0))
if maximum > RECORDED_SPATIAL_POINT_LIMIT:
raise RecordedThreatTimelineError(
"recorded timeline exact point delivery exceeds its declared bound"
)
return maximum
def _index_frame_ledger(path: Path) -> RecordedThreatTimelineIndex:
offsets: list[int] = []
source_times: list[int] = []
+14
View File
@@ -133,6 +133,9 @@ from k1link.web.m48s_fixed_class_detector_lab_api import (
build_m48s_fixed_class_detector_lab_router,
)
from k1link.web.m48t_risk_quality_lab_api import build_m48t_risk_quality_lab_router
from k1link.web.m49_physical_safety_playback_api import (
build_m49_physical_safety_playback_router,
)
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
from k1link.web.map_api import (
@@ -1016,6 +1019,17 @@ app.include_router(
),
)
)
app.include_router(
build_m49_physical_safety_playback_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m49"
/ "physical-safety-playback-results"
),
)
)
app.include_router(
build_m48s_fixed_class_detector_lab_router(
root_provider=lambda: (
@@ -0,0 +1,221 @@
"""Read-only local API for autonomous M49 physical-safety playback artifacts."""
from __future__ import annotations
import copy
import re
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from typing import Any, Final
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import FileResponse
from k1link.laboratory.m49_physical_safety_playback import (
PREFIX,
M49PhysicalSafetyPlayback,
M49PhysicalSafetyPlaybackError,
read_m49_physical_safety_playback,
verify_m49_physical_safety_artifact,
)
RootProvider = Callable[[], Path | None]
RESULT_ID: Final = re.compile(rf"^{re.escape(PREFIX)}[a-f0-9]{{64}}$")
SOURCE_RESULT_ID: Final = re.compile(r"^m49-tgs-full-shadow-[a-f0-9]{64}$")
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/physical-safety-playback"
def build_m49_physical_safety_playback_router(
*, root_provider: RootProvider = lambda: None
) -> APIRouter:
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
def sealed(result_id: str) -> M49PhysicalSafetyPlayback:
root = _configured_root(root_provider)
if root is None or RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="M49 physical-safety playback not found")
candidate = root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(status_code=404, detail="M49 physical-safety playback not found")
try:
resolved = candidate.resolve(strict=True)
if resolved.parent != root:
raise ValueError("result escaped configured root")
return _read_cached(str(resolved), _signature(resolved))
except (M49PhysicalSafetyPlaybackError, OSError, ValueError):
raise HTTPException(
status_code=404, detail="M49 physical-safety playback not found"
) from None
@router.get("/results")
def list_results(
limit: int = Query(default=10, ge=1, le=25),
source_result_id: str | None = Query(default=None),
) -> dict[str, object]:
if source_result_id is not None and SOURCE_RESULT_ID.fullmatch(source_result_id) is None:
raise HTTPException(status_code=422, detail="M49 source result identity is invalid")
root = _configured_root(root_provider)
if root is None:
return _catalog([], configured=False, invalid_total=0)
items: list[dict[str, object]] = []
invalid = 0
for candidate in sorted(root.iterdir()):
if candidate.is_symlink() or not candidate.is_dir() or not RESULT_ID.fullmatch(
candidate.name
):
continue
try:
summary = _summary(
_read_cached(str(candidate.resolve()), _signature(candidate))
)
if source_result_id is None or summary["source_result_id"] == source_result_id:
items.append(summary)
except (M49PhysicalSafetyPlaybackError, OSError, ValueError):
invalid += 1
items.sort(
key=lambda value: (str(value["created_at_utc"]), str(value["result_id"])),
reverse=True,
)
return _catalog(items[:limit], configured=True, invalid_total=invalid)
@router.get("/{result_id}/manifest")
def get_manifest(result_id: str) -> dict[str, object]:
return _project_manifest(sealed(result_id))
@router.get("/{result_id}/tracks/{track_id}")
def get_track(result_id: str, track_id: str) -> FileResponse:
result = sealed(result_id)
playback = _playback(result)
if track_id == "centers":
descriptor = _descriptor(playback.get("centers"), "centers")
elif track_id == "frames":
descriptor = _descriptor(playback.get("frames"), "frames")
else:
raise HTTPException(status_code=404, detail="M49 physical-safety track not found")
return _file_response(result, descriptor)
@router.get("/{result_id}/chunks/{chunk_index}")
def get_chunk(result_id: str, chunk_index: int) -> FileResponse:
result = sealed(result_id)
chunks = _playback(result).get("chunks")
if not isinstance(chunks, list) or chunk_index < 0 or chunk_index >= len(chunks):
raise HTTPException(status_code=404, detail="M49 physical-safety chunk not found")
descriptor = _descriptor(chunks[chunk_index], f"chunk {chunk_index}")
if descriptor.get("index") != chunk_index:
raise HTTPException(status_code=503, detail="M49 physical-safety chunk catalog changed")
return _file_response(result, descriptor)
return router
@lru_cache(maxsize=8)
def _read_cached(root: str, signature: tuple[int, ...]) -> M49PhysicalSafetyPlayback:
del signature
return read_m49_physical_safety_playback(Path(root))
def _file_response(
result: M49PhysicalSafetyPlayback,
descriptor: dict[str, Any],
) -> FileResponse:
try:
path = verify_m49_physical_safety_artifact(result, descriptor)
except (KeyError, OSError, TypeError, ValueError, M49PhysicalSafetyPlaybackError):
raise HTTPException(
status_code=503,
detail="M49 physical-safety playback artifact failed verification",
) from None
return FileResponse(
path,
media_type=str(descriptor.get("media_type") or "application/octet-stream"),
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"Content-Encoding": "identity",
"ETag": f'"{descriptor["sha256"]}"',
"X-Content-Type-Options": "nosniff",
"X-Mission-Core-Worker-Dependency": "none",
},
)
def _playback(result: M49PhysicalSafetyPlayback) -> dict[str, Any]:
playback = result.manifest.get("playback")
if not isinstance(playback, dict):
raise HTTPException(status_code=503, detail="M49 physical-safety playback changed")
return playback
def _descriptor(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise HTTPException(status_code=503, detail=f"M49 physical-safety {label} changed")
return value
def _project_manifest(result: M49PhysicalSafetyPlayback) -> dict[str, object]:
payload = copy.deepcopy(result.manifest)
playback = payload["playback"]
assert isinstance(playback, dict)
centers = playback["centers"]
frames = playback["frames"]
chunks = playback["chunks"]
assert isinstance(centers, dict) and isinstance(frames, dict) and isinstance(chunks, list)
centers["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/tracks/centers"
frames["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/tracks/frames"
for index, value in enumerate(chunks):
assert isinstance(value, dict)
value["url"] = f"{ENDPOINT_ROOT}/{result.result_id}/chunks/{index}"
payload["access"] = "read-only-sealed-local"
return payload
def _summary(result: M49PhysicalSafetyPlayback) -> dict[str, object]:
playback = _playback(result)
return {
"schema_version": "missioncore.m49-physical-safety-playback-summary/v1",
"result_id": result.result_id,
"created_at_utc": result.manifest.get("created_at_utc"),
"source_result_id": result.manifest["identity"]["source_result_id"],
"frame_count": playback.get("frame_count"),
"cell_count": playback.get("cell_count"),
"cell_size_m": playback.get("cell_size_m"),
"radius_m": playback.get("radius_m"),
"chunk_count": len(playback.get("chunks", [])),
"worker_runtime_dependency": False,
"navigation_or_safety_accepted": False,
}
def _catalog(
items: list[dict[str, object]], *, configured: bool, invalid_total: int
) -> dict[str, object]:
return {
"schema_version": "missioncore.m49-physical-safety-playback-catalog/v1",
"configured": configured,
"items": items,
"candidate_total": len(items) + invalid_total,
"invalid_total": invalid_total,
"worker_runtime_dependency": False,
"access": "read-only-sealed-local",
}
def _configured_root(provider: RootProvider) -> Path | None:
value = provider()
if value is None:
return None
raw = value.expanduser().absolute()
if raw.is_symlink() or not raw.is_dir():
return None
return raw.resolve(strict=True)
def _signature(root: Path) -> tuple[int, ...]:
path = root / "manifest.json"
if path.is_symlink() or not path.is_file():
raise ValueError("physical-safety manifest unavailable")
stat = path.stat()
return (stat.st_size, stat.st_mtime_ns)
__all__ = ["build_m49_physical_safety_playback_router"]
+104 -3
View File
@@ -22,7 +22,7 @@ from k1link.perception.threat_replay import (
THREAT_REPLAY_VISUAL_SCHEMA_V2,
ThreatReplayError,
ThreatReplayResult,
read_threat_replay_result,
read_threat_replay_result_metadata,
)
from k1link.perception.threat_timeline import (
RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
@@ -34,6 +34,7 @@ from k1link.sessions import RecordedCameraFrame, SessionIntegrityError
M4_THREAT_CATALOG_SCHEMA: Final = "missioncore.m4-threat-replay-catalog/v1"
M4_THREAT_VIEW_SCHEMA: Final = "missioncore.m4-threat-replay-view/v1"
M4_THREAT_VISUAL_CATALOG_SCHEMA: Final = "missioncore.m4-threat-visual-catalog/v1"
M4_THREAT_PLAYBACK_CHUNK_FRAMES: Final = 24
_RESULT_ID = re.compile(rf"^{THREAT_REPLAY_RESULT_PREFIX}[a-f0-9]{{64}}$")
RootProvider = Callable[[], Path | None]
CameraFrameProvider = Callable[[str, int], RecordedCameraFrame]
@@ -212,13 +213,19 @@ def build_m4_threat_replay_router(
projected = timeline(result_id)
points = projected.store.playback_points_map()
offsets = projected.store.playback_point_offsets()
content_sha256 = hashlib.sha256(memoryview(points).cast("B")).hexdigest()
points_view = memoryview(points).cast("B")
content_sha256 = hashlib.sha256(points_view).hexdigest()
chunks = _playback_chunk_catalog(result_id, points_view, offsets)
return {
"schema_version": "missioncore.recorded-spatial-playback/v1",
"result_id": result_id,
"frame_count": len(offsets) - 1,
"point_count": int(points.shape[0]),
"point_offsets": list(offsets),
"chunk_frame_count": M4_THREAT_PLAYBACK_CHUNK_FRAMES,
"resident_chunk_count_max": 4,
"forward_prefetch_chunk_count": 1,
"chunks": chunks,
"track": {
"id": "points-map-f32",
"url": (
@@ -238,6 +245,42 @@ def build_m4_threat_replay_router(
"access": "read-only-sealed-binary-playback",
}
@router.get(
"/results/{result_id}/timeline/playback/chunks/{chunk_index}",
response_class=Response,
)
def get_timeline_playback_chunk(result_id: str, chunk_index: int) -> Response:
projected = timeline(result_id)
points = projected.store.playback_points_map()
offsets = projected.store.playback_point_offsets()
points_view = memoryview(points).cast("B")
descriptor = _playback_chunk_descriptor(
result_id,
points_view,
offsets,
chunk_index,
)
if descriptor is None:
raise HTTPException(status_code=404, detail="M4.6 playback chunk не найден")
point_start = descriptor["point_start"]
byte_length = descriptor["bytes"]
assert isinstance(point_start, int)
assert isinstance(byte_length, int)
byte_start = point_start * 3 * 4
byte_stop = byte_start + byte_length
return Response(
content=bytes(points_view[byte_start:byte_stop]),
media_type="application/octet-stream",
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"Content-Encoding": "identity",
"Content-Length": str(byte_length),
"ETag": f'"{descriptor["sha256"]}"',
"X-Content-Type-Options": "nosniff",
"X-Uncompressed-Content-Length": str(byte_length),
},
)
@router.get(
"/results/{result_id}/timeline/playback/tracks/points-map-f32",
response_class=StreamingResponse,
@@ -276,13 +319,71 @@ def _binary_chunks(view: memoryview, chunk_size: int = 1024 * 1024) -> Iterator[
yield bytes(view[start : start + chunk_size])
def _playback_chunk_catalog(
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
) -> list[dict[str, object]]:
frame_count = len(offsets) - 1
chunk_count = (
frame_count + M4_THREAT_PLAYBACK_CHUNK_FRAMES - 1
) // M4_THREAT_PLAYBACK_CHUNK_FRAMES
return [
descriptor
for chunk_index in range(chunk_count)
if (
descriptor := _playback_chunk_descriptor(
result_id,
points_view,
offsets,
chunk_index,
)
)
is not None
]
def _playback_chunk_descriptor(
result_id: str,
points_view: memoryview,
offsets: tuple[int, ...],
chunk_index: int,
) -> dict[str, object] | None:
frame_count = len(offsets) - 1
start = chunk_index * M4_THREAT_PLAYBACK_CHUNK_FRAMES
if chunk_index < 0 or start >= frame_count:
return None
count = min(M4_THREAT_PLAYBACK_CHUNK_FRAMES, frame_count - start)
point_start = offsets[start]
point_stop = offsets[start + count]
byte_start = point_start * 3 * 4
byte_stop = point_stop * 3 * 4
payload = points_view[byte_start:byte_stop]
return {
"index": chunk_index,
"start": start,
"count": count,
"point_start": point_start,
"point_count": point_stop - point_start,
"url": (
f"/api/v1/laboratory/m4-threat/results/{result_id}"
f"/timeline/playback/chunks/{chunk_index}"
),
"media_type": "application/octet-stream",
"dtype": "<f4",
"shape": [point_stop - point_start, 3],
"bytes": payload.nbytes,
"sha256": hashlib.sha256(payload).hexdigest(),
}
@lru_cache(maxsize=4)
def _read_threat_result_cached(
root_value: str,
signature: tuple[int, ...],
) -> ThreatReplayResult:
del signature
return read_threat_replay_result(Path(root_value))
return read_threat_replay_result_metadata(Path(root_value))
@lru_cache(maxsize=4)
+199
View File
@@ -0,0 +1,199 @@
from __future__ import annotations
import hashlib
import json
import socket
from pathlib import Path
import numpy as np
from fastapi import FastAPI
from fastapi.testclient import TestClient
from k1link.laboratory.m49_physical_safety_playback import (
CHUNK_HEADER,
CHUNK_MAGIC,
read_m49_physical_safety_playback,
seal_m49_physical_safety_playback,
)
from k1link.web.m49_physical_safety_playback_api import (
build_m49_physical_safety_playback_router,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _sealed_source(root: Path, *, frame_count: int = 65, cell_count: int = 8) -> Path:
root.mkdir()
centers = np.column_stack(
(
np.arange(cell_count, dtype=np.float32) * np.float32(0.15),
np.zeros(cell_count, dtype=np.float32),
)
).astype(np.float32)
states = np.ones((frame_count, cell_count), dtype=np.uint8)
z_bounds = np.zeros((frame_count, cell_count, 2), dtype=np.float32)
z_bounds[..., 1] = np.float32(0.12)
missing_sequence = 17
states[missing_sequence] = 0
z_bounds[missing_sequence] = np.nan
np.save(root / "costmap-cell-centers-xy-m.npy", centers, allow_pickle=False)
np.save(root / "costmap-states.npy", states, allow_pickle=False)
np.save(root / "costmap-z-bounds-m.npy", z_bounds, allow_pickle=False)
frames = [
{
"source_frame_index": sequence,
"session_seconds": sequence / 10,
"sample_available": sequence != missing_sequence,
"eligible_point_count": 0 if sequence == missing_sequence else cell_count,
"ground_point_count": 0 if sequence == missing_sequence else cell_count,
"nonground_point_count": 0,
"rejected_point_count": 0,
"occupied_cell_count": 0,
}
for sequence in range(frame_count)
]
(root / "frames.ndjson").write_text(
"".join(json.dumps(frame, sort_keys=True) + "\n" for frame in frames),
encoding="utf-8",
)
source_identity = {"schema_version": "missioncore.test-sealed-source/v1"}
source_identity_sha256 = hashlib.sha256(
json.dumps(source_identity, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
result_id = "m49-tgs-full-shadow-" + source_identity_sha256
report = {
"schema_version": "missioncore.m49-tgs-full-shadow-report/v1",
"result_id": result_id,
"configuration": {
"coordinate_frame": "map-gravity-local",
"cell_size_m": 0.15,
"radius_m": 12.0,
},
"visual_review": {
"state_codes": {
"UNOBSERVED": 0,
"GROUND_SUPPORT": 1,
"NONGROUND_OCCUPIED": 2,
"UNKNOWN_REJECTED": 3,
}
},
}
(root / "report.json").write_text(json.dumps(report), encoding="utf-8")
artifacts = []
for name in (
"costmap-cell-centers-xy-m.npy",
"costmap-states.npy",
"costmap-z-bounds-m.npy",
"frames.ndjson",
"report.json",
):
path = root / name
artifacts.append(
{
"path": name,
"byte_length": path.stat().st_size,
"sha256": _sha256(path),
}
)
(root / "manifest.json").write_text(
json.dumps(
{
"schema_version": "missioncore.m49-tgs-full-shadow-lab/v1",
"result_id": result_id,
"identity": source_identity,
"identity_sha256": source_identity_sha256,
"artifacts": artifacts,
}
),
encoding="utf-8",
)
return root
def test_local_builder_seals_bounded_chunks_without_worker_network(
monkeypatch,
tmp_path: Path,
) -> None:
source = _sealed_source(tmp_path / "source")
def forbid_network(*_args: object, **_kwargs: object) -> None:
raise AssertionError("local LAB builder attempted network access")
monkeypatch.setattr(socket, "create_connection", forbid_network)
sealed = seal_m49_physical_safety_playback(
source_root=source,
destination_root=tmp_path / "results",
profile_path=(
REPOSITORY_ROOT / "config" / "perception" / "m49-physical-safety-shadow-v0.json"
),
created_at_utc="2026-08-27T12:00:00Z",
)
assert sealed.manifest["execution"] == {
"execution_class": "local-sequential-offline",
"worker_role": "realtime-only",
"worker_runtime_dependency": False,
"worker_requests_required": 0,
}
playback = sealed.manifest["playback"]
assert playback["cell_size_m"] == 0.15
assert playback["chunk_frame_count"] == 32
assert playback["startup_prebuffer_chunk_count"] == 2
assert playback["resident_chunk_count_max"] == 3
assert [(chunk["start"], chunk["count"]) for chunk in playback["chunks"]] == [
(0, 32),
(32, 32),
(64, 1),
]
first = (sealed.root / playback["chunks"][0]["path"]).read_bytes()
magic, start, count, cells, state_bytes, z_bytes = CHUNK_HEADER.unpack_from(first)
assert (magic, start, count, cells) == (CHUNK_MAGIC, 0, 32, 8)
assert state_bytes == 32 * 8
assert z_bytes == 32 * 8 * 2 * 4
assert read_m49_physical_safety_playback(sealed.root).result_id == sealed.result_id
def test_local_api_serves_only_hash_verified_sealed_tracks(tmp_path: Path) -> None:
source = _sealed_source(tmp_path / "source")
destination = tmp_path / "results"
sealed = seal_m49_physical_safety_playback(
source_root=source,
destination_root=destination,
profile_path=(
REPOSITORY_ROOT / "config" / "perception" / "m49-physical-safety-shadow-v0.json"
),
created_at_utc="2026-08-27T12:00:00Z",
)
app = FastAPI()
app.include_router(
build_m49_physical_safety_playback_router(root_provider=lambda: destination)
)
client = TestClient(app)
manifest_response = client.get(
f"/api/v1/laboratory/m49/physical-safety-playback/{sealed.result_id}/manifest"
)
assert manifest_response.status_code == 200
manifest = manifest_response.json()
assert manifest["execution"]["worker_requests_required"] == 0
assert manifest["access"] == "read-only-sealed-local"
assert "worker" not in manifest["playback"]["centers"]["url"]
assert client.get(manifest["playback"]["centers"]["url"]).status_code == 200
assert client.get(manifest["playback"]["chunks"][0]["url"]).status_code == 200
source_result_id = sealed.manifest["identity"]["source_result_id"]
catalog = client.get(
"/api/v1/laboratory/m49/physical-safety-playback/results",
params={"source_result_id": source_result_id},
)
assert catalog.status_code == 200
assert [item["result_id"] for item in catalog.json()["items"]] == [sealed.result_id]
chunk_path = sealed.root / sealed.manifest["playback"]["chunks"][0]["path"]
damaged = bytearray(chunk_path.read_bytes())
damaged[-1] ^= 1
chunk_path.write_bytes(damaged)
assert client.get(manifest["playback"]["chunks"][0]["url"]).status_code == 503
+38 -1
View File
@@ -1,11 +1,18 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from fastapi.routing import APIRoute
from pytest import MonkeyPatch
from k1link.perception.threat_replay import read_threat_replay_result
from k1link.perception.geometry import RecordedGeometryStore
from k1link.perception.threat_replay import (
read_threat_replay_result,
read_threat_replay_result_metadata,
)
from k1link.perception.threat_timeline import RecordedThreatTimeline
from k1link.sessions import RecordedCameraFrame
from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
@@ -128,6 +135,9 @@ def test_m4_6_timeline_is_indexed_and_spatial_evidence_is_chunked() -> None:
get_timeline = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}/timeline")
get_chunk = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}/timeline/chunk")
get_playback = _endpoint("/api/v1/laboratory/m4-threat/results/{result_id}/timeline/playback")
get_playback_chunk = _endpoint(
"/api/v1/laboratory/m4-threat/results/{result_id}/timeline/playback/chunks/{chunk_index}"
)
timeline = get_timeline(RESULT_ID)
assert timeline["schema_version"] == "missioncore.recorded-spatial-evidence-timeline/v1"
@@ -178,6 +188,33 @@ def test_m4_6_timeline_is_indexed_and_spatial_evidence_is_chunked() -> None:
assert playback["track"]["shape"] == [9_207_270, 3]
assert playback["track"]["bytes"] == 110_487_240
assert len(playback["track"]["sha256"]) == 64
assert playback["chunk_frame_count"] == 24
assert playback["resident_chunk_count_max"] == 4
assert playback["forward_prefetch_chunk_count"] == 1
assert len(playback["chunks"]) == 188
first_chunk = playback["chunks"][0]
assert first_chunk["start"] == 0
assert first_chunk["count"] == 24
assert first_chunk["bytes"] < 3 * 1024 * 1024
chunk_response = get_playback_chunk(RESULT_ID, 0)
assert len(chunk_response.body) == first_chunk["bytes"]
assert hashlib.sha256(chunk_response.body).hexdigest() == first_chunk["sha256"]
def test_m4_6_timeline_metadata_defers_large_geometry_archives(
monkeypatch: MonkeyPatch,
) -> None:
result = read_threat_replay_result_metadata(RESULTS_ROOT / RESULT_ID)
def reject_eager_load(*_args: object, **_kwargs: object) -> RecordedGeometryStore:
raise AssertionError("timeline metadata eagerly loaded the geometry archives")
monkeypatch.setattr(RecordedGeometryStore, "from_repository", reject_eager_load)
timeline = RecordedThreatTimeline(repository_root=REPOSITORY_ROOT, result=result)
assert timeline.metadata()["frame_count"] == 4489
assert timeline.metadata()["maximum_source_points_per_frame"] == 3092
assert timeline._store is None
def test_m4_6_exact_camera_endpoint_is_bound_to_selected_visual_sequence() -> None: