feat(lab): publish fail-closed TGS evidence

This commit is contained in:
DCCONSTRUCTIONS
2026-08-26 21:32:35 +03:00
parent 67d5d6fa05
commit 6544d9e918
27 changed files with 2167 additions and 73 deletions
@@ -41,8 +41,25 @@ export interface LaboratoryMetricCorridorVisual {
halfWidthM: number;
}
export type LaboratoryMetricCellState =
| "unobserved"
| "ground-support"
| "nonground-occupied"
| "unknown-rejected";
export interface LaboratoryMetricCellEvidence {
centerBodyXyM: readonly [number, number];
zBoundsM: readonly [number | null, number | null];
state: LaboratoryMetricCellState;
}
export interface LaboratoryMetricLegendEntry {
id: LaboratoryMetricDecision | "context" | "local-surface" | "rolling" | "low-step";
id: LaboratoryMetricDecision
| LaboratoryMetricCellState
| "context"
| "local-surface"
| "rolling"
| "low-step";
label: string;
}
@@ -197,6 +214,9 @@ LaboratoryMetricEvidenceSceneHandle,
pointSemanticClassIds?: readonly (number | null)[];
semanticClasses?: readonly RecordedEvidenceSemanticClass[];
semanticPalette?: readonly RecordedEvidenceSemanticPaletteEntry[];
classifiedCells?: readonly LaboratoryMetricCellEvidence[];
classifiedCellSizeM?: number;
showClassifiedCells?: boolean;
}
>(function LaboratoryMetricEvidenceScene({
pointCloudBodyXyzM,
@@ -214,6 +234,9 @@ LaboratoryMetricEvidenceSceneHandle,
pointSemanticClassIds,
semanticClasses,
semanticPalette,
classifiedCells = [],
classifiedCellSizeM = 0.45,
showClassifiedCells = true,
}, ref) {
const hostRef = useRef<HTMLDivElement | null>(null);
const sceneRef = useRef<THREE.Scene | null>(null);
@@ -374,6 +397,63 @@ LaboratoryMetricEvidenceSceneHandle,
));
}
if (showClassifiedCells && classifiedCells.length) {
const cellsByState = new Map<LaboratoryMetricCellState, LaboratoryMetricCellEvidence[]>();
for (const cell of classifiedCells) {
const cells = cellsByState.get(cell.state) ?? [];
cells.push(cell);
cellsByState.set(cell.state, cells);
}
for (const [state, cells] of cellsByState) {
const color = state === "ground-support"
? tokenColor(host, "--nodedc-success-rgb", [181, 255, 90])
: state === "nonground-occupied"
? tokenColor(host, "--nodedc-danger-rgb", [255, 104, 112])
: state === "unknown-rejected"
? tokenColor(host, "--nodedc-warning-rgb", [255, 197, 92])
: tokenColor(host, "--nodedc-text-muted", [96, 99, 106]);
const geometry = new THREE.BoxGeometry(
classifiedCellSizeM * 0.92,
1,
classifiedCellSizeM * 0.92,
);
const material = new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: state === "unobserved" ? 0.035 : state === "ground-support" ? 0.12 : 0.24,
depthWrite: false,
});
const mesh = new THREE.InstancedMesh(geometry, material, cells.length);
const matrix = new THREE.Matrix4();
const scale = new THREE.Vector3(1, 1, 1);
const rotation = new THREE.Quaternion();
cells.forEach((cell, index) => {
const minimum = cell.zBoundsM[0];
const maximum = cell.zBoundsM[1];
const height = minimum === null || maximum === null
? 0.018
: Math.max(0.018, maximum - minimum);
const centerZ = minimum === null || maximum === null
? -0.012
: (minimum + maximum) / 2;
const [sceneX, sceneY, sceneZ] = scenePoint([
cell.centerBodyXyM[0],
cell.centerBodyXyM[1],
centerZ,
]);
scale.set(1, height, 1);
matrix.compose(
new THREE.Vector3(sceneX, sceneY, sceneZ),
rotation,
scale,
);
mesh.setMatrixAt(index, matrix);
});
mesh.instanceMatrix.needsUpdate = true;
content.add(mesh);
}
}
for (const obstacle of obstacles) {
if (
(
@@ -451,7 +531,10 @@ LaboratoryMetricEvidenceSceneHandle,
pointSemanticClassIds,
semanticClasses,
semanticPalette,
classifiedCells,
classifiedCellSizeM,
showCurrentIncrement,
showClassifiedCells,
showLocalSurface,
showRollingMap,
showLowStep,
@@ -563,7 +646,9 @@ LaboratoryMetricEvidenceSceneHandle,
});
})();
const metricLegendEntries = laboratoryMetricLegendEntries({
pointCloudCount: pointCloudBodyXyzM.length,
pointCloudCount: pointSemanticClassIds?.every((item) => item !== null)
? 0
: pointCloudBodyXyzM.length,
localSurfaceCount: localSurfaceBodyXyzM.length,
obstacles,
showCurrentIncrement,
@@ -571,6 +656,28 @@ LaboratoryMetricEvidenceSceneHandle,
showRollingMap,
showLowStep,
});
const classifiedLegendEntries = (() => {
if (!showClassifiedCells || !classifiedCells.length) return [];
const states = new Set(classifiedCells.map((cell) => cell.state));
return [
states.has("ground-support")
? { id: "ground-support" as const, label: "Ground support" }
: null,
states.has("nonground-occupied")
? { id: "nonground-occupied" as const, label: "Non-ground occupied" }
: null,
states.has("unknown-rejected")
? { id: "unknown-rejected" as const, label: "Unknown / rejected" }
: null,
states.has("unobserved")
? { id: "unobserved" as const, label: "Unobserved" }
: null,
]
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
.filter((entry) => !semanticLegendEntries.some(
(semanticEntry) => semanticEntry.label === entry.label,
));
})();
return (
<div className="laboratory-metric-evidence-scene">
@@ -581,6 +688,9 @@ LaboratoryMetricEvidenceSceneHandle,
{metricLegendEntries.map((entry) => (
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
))}
{classifiedLegendEntries.map((entry) => (
<span key={entry.id} data-decision={entry.id}>{entry.label}</span>
))}
{semanticLegendEntries.map((entry) => (
<span
key={entry.id}
@@ -46,6 +46,7 @@ import { fetchM48StaticOccupancyQualification } from "./m48StaticOccupancyQualif
import { fetchM48R3StaticOccupancyShadow } from "./m48r3StaticOccupancyShadow";
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
import { fetchM49TgsFailClosedResult } from "./m49TgsFailClosed";
export type AdvancedLaboratoryWorkId =
| "m48-object-centric-quality"
@@ -54,6 +55,7 @@ export type AdvancedLaboratoryWorkId =
| "m48r3-static-occupancy-shadow"
| "m48s-fixed-class-detector"
| "m48t-risk-quality-temporal"
| "m49-tgs-fail-closed-evidence"
| "m47-reference-graph-shadow"
| "m4-replay-threat"
| "l3-pointpillars-visual-audit"
@@ -102,6 +104,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"m48r3-static-occupancy-shadow",
"m48s-fixed-class-detector",
"m48t-risk-quality-temporal",
"m49-tgs-fail-closed-evidence",
"m47-reference-graph-shadow",
"m4-replay-threat",
"l3-pointpillars-visual-audit",
@@ -145,6 +148,7 @@ const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"m48r3-static-occupancy-shadow": "m48r3-static-occupancy-shadow",
"m48s-fixed-class-detector": "m48s-fixed-class-detector-lab",
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
"m49-tgs-fail-closed-evidence": "m49-tgs-fail-closed",
"m47-reference-graph-shadow": "m47-reference-graph-lab",
"m4-replay-threat": "m4-threat-replay",
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
@@ -196,6 +200,7 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
m48r3StaticOccupancy: null,
m48s: null,
m48t: null,
m49Tgs: null,
m4Threat: null,
l3: null,
l31: null,
@@ -326,6 +331,7 @@ export function advancedLaboratoryResultAvailable(
: workId === "m48r3-static-occupancy-shadow" ? results.m48r3StaticOccupancy !== null
: workId === "m48s-fixed-class-detector" ? results.m48s !== null
: workId === "m48t-risk-quality-temporal" ? results.m48t !== null
: workId === "m49-tgs-fail-closed-evidence" ? results.m49Tgs !== null
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
: workId === "m4-replay-threat" ? results.m4Threat !== null
: workId === "l3-pointpillars-visual-audit" ? results.l3 !== null
@@ -393,6 +399,9 @@ export async function fetchAdvancedLaboratoryResult(
} else if (workId === "m48t-risk-quality-temporal") {
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8T LAB identity не выбрана.");
results.m48t = await fetchM48TRiskQualityResult(resultId, { fetcher, signal });
} else if (workId === "m49-tgs-fail-closed-evidence") {
if (!resultId) throw new AdvancedLaboratoryContractError("M4.9 TGS LAB identity не выбрана.");
results.m49Tgs = await fetchM49TgsFailClosedResult(resultId, { fetcher, signal });
} else if (workId === "m47-reference-graph-shadow") {
if (!resultId) {
throw new AdvancedLaboratoryContractError("M4.7 LAB identity не выбрана.");
@@ -40,6 +40,7 @@ import type { M48StaticOccupancyQualificationResult } from "./m48StaticOccupancy
import type { M48R3StaticOccupancyShadowResult } from "./m48r3StaticOccupancyShadow";
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
import type { M49TgsFailClosedResult } from "./m49TgsFailClosed";
export interface AdvancedLaboratoryResults {
m47Graph: M47ReferenceGraphLabResult | null;
@@ -49,6 +50,7 @@ export interface AdvancedLaboratoryResults {
m48r3StaticOccupancy: M48R3StaticOccupancyShadowResult | null;
m48s: M48SFixedClassDetectorResult | null;
m48t: M48TRiskQualityResult | null;
m49Tgs: M49TgsFailClosedResult | null;
m4Threat: M4ThreatReplayResult | null;
l3: L3PointPillarsVisualAuditResult | null;
l31: L31PointPillarsRavnovesResult | null;
@@ -969,7 +969,7 @@ export async function fetchAdvancedLaboratoryResults({
return {
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
m48r3StaticOccupancy: null,
m48s: null, m48t: null, m4Threat: null,
m48s: null, m48t: null, m49Tgs: null, m4Threat: null,
l3: null, l31: null, l32: null, l33: null,
e31,
e32,
@@ -0,0 +1,349 @@
import type { LaboratoryFetch } from "./advancedResults";
const RESULT_ID = /^m49-tgs-fail-closed-[a-f0-9]{64}$/;
export type M49TgsProfile = "current_increment" | "causal_rolling_1s";
export type M49TgsStateCode = 0 | 1 | 2 | 3;
export type M49TgsPointStateCode = 1 | 2 | 3;
export interface M49TgsAnchorSummary {
anchorFrameIndex: number;
slot: number;
pointCount: number;
groundPointCount: number;
nongroundPointCount: number;
rejectedPointCount: number;
groundCellCount: number;
nongroundCellCount: number;
rejectedCellCount: number;
unobservedCellCount: number;
allPointsAccounted: true;
}
export interface M49TgsFailClosedResult {
resultId: string;
createdAtUtc: string;
source: {
sourceId: "RAVNOVES00";
sourceSessionId: "20260720T065719Z_viewer_live";
sourcePackSha256: string;
linkedVisualResultId: string;
anchorFrameIndices: readonly number[];
};
configuration: {
profileId: string;
configSha256: string;
coordinateFrame: "map-gravity-local";
primaryProfile: "causal_rolling_1s";
cellSizeM: number;
radiusM: number;
};
execution: {
worker: "Worker 006";
device: "cpu";
gpuUsed: false;
wrapperElapsedSeconds: number;
};
metrics: {
anchorCount: number;
anchorProfileCount: number;
allEligiblePointsAccounted: true;
costmapCellCount: number;
processWallCurrentP50Ms: number;
processWallCurrentMaxMs: number;
processWallRollingP50Ms: number;
processWallRollingMaxMs: number;
processMaxRssKib: number;
primary: readonly M49TgsAnchorSummary[];
};
acceptance: {
representationComplete: true;
allPointsAccounted: true;
aosAbsent: true;
gpuAbsent: true;
visualQualityAccepted: false;
traversabilityAccepted: false;
};
decision: {
state: "visual-review-required";
candidateRetained: true;
nextAction: string;
};
limitations: readonly string[];
}
export interface M49TgsAnchorSpatial {
resultId: string;
anchorFrameIndex: number;
sourceSequence: number;
profile: M49TgsProfile;
coordinateFrame: "map-gravity-local";
pointsXyzM: readonly (readonly [number, number, number])[];
pointStates: readonly M49TgsPointStateCode[];
costmap: {
cellSizeM: number;
radiusM: number;
centersXyM: readonly (readonly [number, number])[];
states: readonly M49TgsStateCode[];
zBoundsM: readonly (readonly [number | null, number | null])[];
};
allPointsAccounted: true;
aosUsed: false;
}
export class M49TgsContractError extends Error {}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new M49TgsContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function text(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new M49TgsContractError(`${label}: ожидалась строка.`);
}
return value;
}
function exact<T extends string | boolean>(value: unknown, expected: T, label: string): T {
if (value !== expected) throw new M49TgsContractError(`${label}: нарушен контракт.`);
return expected;
}
function numberValue(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new M49TgsContractError(`${label}: ожидалось число.`);
}
return value;
}
function integer(value: unknown, label: string): number {
const parsed = numberValue(value, label);
if (!Number.isSafeInteger(parsed) || parsed < 0) {
throw new M49TgsContractError(`${label}: ожидалось целое число.`);
}
return parsed;
}
function sha256(value: unknown, label: string): string {
const parsed = text(value, label);
if (!/^[a-f0-9]{64}$/.test(parsed)) {
throw new M49TgsContractError(`${label}: неверный SHA-256.`);
}
return parsed;
}
function resultId(value: unknown): string {
const parsed = text(value, "M49 result id");
if (!RESULT_ID.test(parsed)) throw new M49TgsContractError("M49 identity недопустима.");
return parsed;
}
function numbers(value: unknown, size: number, label: string): number[] {
if (!Array.isArray(value) || value.length !== size) {
throw new M49TgsContractError(`${label}: неверная размерность.`);
}
return value.map((item) => numberValue(item, label));
}
function anchor(value: unknown, label: string): M49TgsAnchorSummary {
const row = objectValue(value, label);
exact(row.profile_id, "causal_rolling_1s", `${label}.profile`);
exact(row.all_points_accounted, true, `${label}.accounting`);
const parsed: M49TgsAnchorSummary = {
anchorFrameIndex: integer(row.anchor_frame_index, `${label}.anchor`),
slot: integer(row.slot, `${label}.slot`),
pointCount: integer(row.point_count, `${label}.points`),
groundPointCount: integer(row.ground_point_count, `${label}.ground points`),
nongroundPointCount: integer(row.nonground_point_count, `${label}.nonground points`),
rejectedPointCount: integer(row.rejected_point_count, `${label}.rejected points`),
groundCellCount: integer(row.ground_cell_count, `${label}.ground cells`),
nongroundCellCount: integer(row.nonground_cell_count, `${label}.nonground cells`),
rejectedCellCount: integer(row.rejected_cell_count, `${label}.rejected cells`),
unobservedCellCount: integer(row.unobserved_cell_count, `${label}.unobserved cells`),
allPointsAccounted: true,
};
if (
parsed.groundPointCount + parsed.nongroundPointCount + parsed.rejectedPointCount
!== parsed.pointCount
|| parsed.groundCellCount + parsed.nongroundCellCount
+ parsed.rejectedCellCount + parsed.unobservedCellCount !== 2244
) {
throw new M49TgsContractError(`${label}: fail-closed accounting нарушен.`);
}
return parsed;
}
export async function fetchM49TgsFailClosedResult(
id: string,
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<M49TgsFailClosedResult> {
if (!RESULT_ID.test(id)) throw new M49TgsContractError("M49 identity недопустима.");
const response = await fetcher(
`/api/v1/laboratory/m49/tgs-fail-closed/${encodeURIComponent(id)}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) throw new M49TgsContractError(`M49 TGS недоступен: HTTP ${response.status}.`);
const payload = objectValue(await response.json(), "M49 TGS");
exact(payload.schema_version, "missioncore.m49-tgs-fail-closed-view/v1", "M49 schema");
exact(payload.result_id, id, "M49 result");
exact(payload.ground_truth, false, "M49 ground truth");
exact(payload.access, "read-only", "M49 access");
const source = objectValue(payload.source, "M49 source");
const configuration = objectValue(payload.configuration, "M49 configuration");
const execution = objectValue(payload.execution, "M49 execution");
const metrics = objectValue(payload.metrics, "M49 metrics");
const acceptance = objectValue(payload.acceptance, "M49 acceptance");
const decision = objectValue(payload.decision, "M49 decision");
if (!Array.isArray(source.anchor_frame_indices) || !Array.isArray(metrics.primary)) {
throw new M49TgsContractError("M49 anchors: ожидался массив.");
}
if (!Array.isArray(payload.limitations)) {
throw new M49TgsContractError("M49 limitations: ожидался массив.");
}
const anchorFrameIndices = source.anchor_frame_indices.map(
(item, index) => integer(item, `M49 anchor ${index}`),
);
const primary = metrics.primary.map((item, index) => anchor(item, `M49 primary ${index}`));
if (
anchorFrameIndices.length !== 10
|| new Set(anchorFrameIndices).size !== 10
|| primary.length !== 10
|| integer(metrics.anchor_count, "M49 anchor count") !== 10
|| integer(metrics.anchor_profile_count, "M49 profile count") !== 20
|| integer(metrics.costmap_cell_count, "M49 costmap cells") !== 2244
|| primary.some((item, index) => item.anchorFrameIndex !== anchorFrameIndices[index])
) {
throw new M49TgsContractError("M49 anchor set или costmap contract нарушен.");
}
return {
resultId: resultId(payload.result_id),
createdAtUtc: text(payload.created_at_utc, "M49 created"),
source: {
sourceId: exact(source.source_id, "RAVNOVES00", "M49 source id"),
sourceSessionId: exact(
source.source_session_id,
"20260720T065719Z_viewer_live",
"M49 source session",
),
sourcePackSha256: sha256(source.source_pack_sha256, "M49 source pack"),
linkedVisualResultId: text(source.linked_visual_result_id, "M49 visual result"),
anchorFrameIndices,
},
configuration: {
profileId: text(configuration.profile_id, "M49 profile"),
configSha256: sha256(configuration.config_sha256, "M49 config"),
coordinateFrame: exact(configuration.coordinate_frame, "map-gravity-local", "M49 frame"),
primaryProfile: exact(configuration.primary_profile, "causal_rolling_1s", "M49 primary"),
cellSizeM: numberValue(configuration.cell_size_m, "M49 cell size"),
radiusM: numberValue(configuration.radius_m, "M49 radius"),
},
execution: {
worker: exact(execution.worker, "Worker 006", "M49 worker"),
device: exact(execution.device, "cpu", "M49 device"),
gpuUsed: exact(execution.gpu_used, false, "M49 GPU"),
wrapperElapsedSeconds: numberValue(execution.wrapper_elapsed_seconds, "M49 wall"),
},
metrics: {
anchorCount: 10,
anchorProfileCount: 20,
allEligiblePointsAccounted: exact(metrics.all_eligible_points_accounted, true, "M49 accounting"),
costmapCellCount: 2244,
processWallCurrentP50Ms: numberValue(metrics.process_wall_current_p50_ms, "M49 current p50"),
processWallCurrentMaxMs: numberValue(metrics.process_wall_current_max_ms, "M49 current max"),
processWallRollingP50Ms: numberValue(metrics.process_wall_rolling_p50_ms, "M49 rolling p50"),
processWallRollingMaxMs: numberValue(metrics.process_wall_rolling_max_ms, "M49 rolling max"),
processMaxRssKib: integer(metrics.process_max_rss_kib, "M49 RSS"),
primary,
},
acceptance: {
representationComplete: exact(acceptance.representation_complete, true, "M49 representation"),
allPointsAccounted: exact(acceptance.all_points_accounted, true, "M49 points"),
aosAbsent: exact(acceptance.aos_absent, true, "M49 AOS"),
gpuAbsent: exact(acceptance.gpu_absent, true, "M49 GPU absent"),
visualQualityAccepted: exact(acceptance.visual_quality_accepted, false, "M49 visual quality"),
traversabilityAccepted: exact(acceptance.traversability_accepted, false, "M49 traversability"),
},
decision: {
state: exact(decision.state, "visual-review-required", "M49 decision"),
candidateRetained: exact(decision.candidate_retained, true, "M49 retained"),
nextAction: text(decision.next_action, "M49 next action"),
},
limitations: payload.limitations.map((item, index) => text(item, `M49 limitation ${index}`)),
};
}
export async function fetchM49TgsAnchorSpatial(
id: string,
anchorFrameIndex: number,
profile: M49TgsProfile = "causal_rolling_1s",
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
): Promise<M49TgsAnchorSpatial> {
if (!RESULT_ID.test(id) || !Number.isSafeInteger(anchorFrameIndex) || anchorFrameIndex < 0) {
throw new M49TgsContractError("M49 anchor identity недопустима.");
}
const response = await fetcher(
`/api/v1/laboratory/m49/tgs-fail-closed/${encodeURIComponent(id)}/anchors/${anchorFrameIndex}/spatial?profile=${encodeURIComponent(profile)}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) throw new M49TgsContractError(`M49 anchor недоступен: HTTP ${response.status}.`);
const payload = objectValue(await response.json(), "M49 anchor");
exact(payload.schema_version, "missioncore.m49-tgs-anchor-spatial/v1", "M49 anchor schema");
exact(payload.result_id, id, "M49 anchor result");
exact(payload.coordinate_frame, "map-gravity-local", "M49 anchor frame");
exact(payload.all_points_accounted, true, "M49 anchor accounting");
exact(payload.aos_used, false, "M49 anchor AOS");
if (!Array.isArray(payload.points_xyz_m) || !Array.isArray(payload.point_states)) {
throw new M49TgsContractError("M49 points: ожидался массив.");
}
const costmap = objectValue(payload.costmap, "M49 costmap");
if (!Array.isArray(costmap.centers_xy_m) || !Array.isArray(costmap.states) || !Array.isArray(costmap.z_bounds_m)) {
throw new M49TgsContractError("M49 costmap arrays: нарушен контракт.");
}
const points = payload.points_xyz_m.map((item, index) => numbers(item, 3, `M49 point ${index}`) as [number, number, number]);
const pointStates = payload.point_states.map((item, index) => {
const state = integer(item, `M49 point state ${index}`);
if (state !== 1 && state !== 2 && state !== 3) throw new M49TgsContractError("M49 point state неизвестен.");
return state;
});
if (points.length !== pointStates.length) throw new M49TgsContractError("M49 point accounting нарушен.");
const centers = costmap.centers_xy_m.map((item, index) => numbers(item, 2, `M49 cell ${index}`) as [number, number]);
const states = costmap.states.map((item, index) => {
const state = integer(item, `M49 cell state ${index}`);
if (state !== 0 && state !== 1 && state !== 2 && state !== 3) throw new M49TgsContractError("M49 cell state неизвестен.");
return state;
});
const zBounds = costmap.z_bounds_m.map((item, index) => {
if (!Array.isArray(item) || item.length !== 2) throw new M49TgsContractError(`M49 z ${index}: размерность.`);
return item.map((value) => value === null ? null : numberValue(value, `M49 z ${index}`)) as [number | null, number | null];
});
if (
centers.length !== 2244
|| centers.length !== states.length
|| centers.length !== zBounds.length
|| integer(payload.anchor_frame_index, "M49 anchor frame") !== anchorFrameIndex
|| integer(payload.source_sequence, "M49 source sequence") !== anchorFrameIndex
) {
throw new M49TgsContractError("M49 costmap accounting нарушен.");
}
return {
resultId: id,
anchorFrameIndex,
sourceSequence: anchorFrameIndex,
profile: exact(payload.profile, profile, "M49 profile"),
coordinateFrame: "map-gravity-local",
pointsXyzM: points,
pointStates,
costmap: {
cellSizeM: numberValue(costmap.cell_size_m, "M49 cell size"),
radiusM: numberValue(costmap.radius_m, "M49 radius"),
centersXyM: centers,
states,
zBoundsM: zBounds,
},
allPointsAccounted: true,
aosUsed: false,
};
}
@@ -91,6 +91,11 @@
justify-content: space-between;
}
.m4-replay-threat-evidence-viewer[data-mode-controls="content"]
.m4-replay-threat-visual__pane-toolbar[data-pane-toolbar="spatial"] {
right: 3.8rem;
}
.m4-replay-threat-visual__pane-toolbar > *,
.m4-replay-threat-visual__spatial-toolbar-end > * {
pointer-events: auto;
@@ -350,3 +355,20 @@
background: rgb(var(--nodedc-accent-rgb));
opacity: 0.72;
}
.laboratory-metric-evidence-scene__legend span[data-decision="ground-support"]::before {
background: rgb(var(--nodedc-success-rgb));
}
.laboratory-metric-evidence-scene__legend span[data-decision="nonground-occupied"]::before {
background: rgb(var(--nodedc-danger-rgb));
}
.laboratory-metric-evidence-scene__legend span[data-decision="unknown-rejected"]::before {
background: rgb(var(--nodedc-warning-rgb));
}
.laboratory-metric-evidence-scene__legend span[data-decision="unobserved"]::before {
background: var(--nodedc-text-muted);
opacity: 0.42;
}
@@ -48,6 +48,7 @@ import { M48StaticOccupancyQualificationResultView } from "./M48StaticOccupancyQ
import { M48R3StaticOccupancyShadowResultView } from "./M48R3StaticOccupancyShadowResult";
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
import { M49TgsFailClosedResultView } from "./M49TgsFailClosedResult";
export { isAdvancedLaboratoryWorkId };
export type { AdvancedLaboratoryWorkId };
@@ -108,6 +109,9 @@ export function AdvancedLaboratoryResult({
if (workId === "m48t-risk-quality-temporal" && results.m48t) {
return <M48TRiskQualityResultView rigLabel={rigLabel} result={results.m48t} />;
}
if (workId === "m49-tgs-fail-closed-evidence" && results.m49Tgs) {
return <M49TgsFailClosedResultView rigLabel={rigLabel} result={results.m49Tgs} />;
}
if (workId === "m47-reference-graph-shadow" && results.m47Graph) {
return <M47ReferenceGraphResultView rigLabel={rigLabel} result={results.m47Graph} />;
}
@@ -0,0 +1,152 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type {
RecordedEvidenceSemanticClass,
RecordedEvidenceSemanticPaletteEntry,
} from "../../components/laboratory/RecordedEvidenceSemanticMaskOverlay";
import {
fetchM49TgsAnchorSpatial,
type M49TgsAnchorSpatial,
type M49TgsFailClosedResult,
type M49TgsStateCode,
} from "../../core/laboratory/m49TgsFailClosed";
import {
M4ReplayThreatVisual,
type M4ReplayClassifiedSpatialFrame,
type M4ReplayThreatReviewAnchor,
} from "./M4ReplayThreatVisual";
const CLASSES: readonly RecordedEvidenceSemanticClass[] = [
{ id: 1, label: "Ground support" },
{ id: 2, label: "Non-ground occupied" },
{ id: 3, label: "Unknown / rejected" },
];
const PALETTE: readonly RecordedEvidenceSemanticPaletteEntry[] = [
{ classId: 1, color: { kind: "token", token: "--nodedc-success-rgb" } },
{ classId: 2, color: { kind: "token", token: "--nodedc-danger-rgb" } },
{ classId: 3, color: { kind: "token", token: "--nodedc-warning-rgb" } },
];
function cellState(code: M49TgsStateCode): 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
: "M49 TGS spatial evidence недоступно.";
}
export function M49TgsFailClosedEvidence({
result,
}: {
result: M49TgsFailClosedResult;
}) {
const [activeSequence, setActiveSequence] = useState<number | null>(null);
const [spatial, setSpatial] = useState<M49TgsAnchorSpatial | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const spatialCacheRef = useRef(new Map<string, M49TgsAnchorSpatial>());
const anchorSequences = useMemo(
() => new Set(result.metrics.primary.map((item) => item.anchorFrameIndex)),
[result.metrics.primary],
);
const expectedAtSequence = activeSequence !== null && anchorSequences.has(activeSequence);
useEffect(() => {
if (activeSequence === null || !anchorSequences.has(activeSequence)) {
setSpatial(null);
setLoading(false);
setError(null);
return;
}
const cacheKey = `${result.resultId}:${activeSequence}`;
const cached = spatialCacheRef.current.get(cacheKey);
if (cached) {
setSpatial(cached);
setLoading(false);
setError(null);
return;
}
const controller = new AbortController();
setSpatial(null);
setLoading(true);
setError(null);
void fetchM49TgsAnchorSpatial(
result.resultId,
activeSequence,
"causal_rolling_1s",
{ signal: controller.signal },
)
.then((next) => {
if (!controller.signal.aborted) {
spatialCacheRef.current.set(cacheKey, next);
setSpatial(next);
}
})
.catch((caught: unknown) => {
if (!controller.signal.aborted) setError(message(caught));
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [activeSequence, anchorSequences, result.resultId]);
const reviewAnchors = useMemo<readonly M4ReplayThreatReviewAnchor[]>(
() => result.metrics.primary.map((item) => ({
id: `m49-tgs-${item.anchorFrameIndex}`,
sourceSequence: item.anchorFrameIndex,
extentXyxyNormalized: [0, 0, 0, 0],
matchedAtThreshold: false,
statusLabel: "визуальная проверка",
})),
[result.metrics.primary],
);
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
if (!spatial) return null;
return {
sourceSequence: spatial.sourceSequence,
pointsMapGravityLocalXyzM: spatial.pointsXyzM,
pointClassIds: spatial.pointStates,
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);
}, []);
return (
<M4ReplayThreatVisual
resultId={result.source.linkedVisualResultId}
reviewAnchors={reviewAnchors}
showReviewAnchorBoxes={false}
reviewLabel="10 gravity-aligned TGS anchors"
evidenceLabel="M49 · TGS fail-closed"
initialSpatialMode="3d"
onActiveSequenceChange={handleSequenceChange}
classifiedSpatialLayer={{
label: "TGS fail-closed · causal rolling 1 s",
pointLayerLabel: "TGS POINTS",
cellLayerLabel: "COSTMAP",
expectedAtSequence,
frame: classifiedFrame,
loading,
error,
}}
/>
);
}
@@ -0,0 +1,92 @@
import {
LaboratoryEvidence,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
} from "../../components/laboratory/LaboratoryPresentation";
import type { M49TgsFailClosedResult } from "../../core/laboratory/m49TgsFailClosed";
import { M49TgsFailClosedEvidence } from "./M49TgsFailClosedEvidence";
function number(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
}
export function M49TgsFailClosedResultView({
rigLabel,
result,
}: {
rigLabel: string;
result: M49TgsFailClosedResult;
}) {
const worst = [...result.metrics.primary].sort(
(left, right) => (
right.nongroundPointCount / Math.max(right.pointCount, 1)
- left.nongroundPointCount / Math.max(left.pointCount, 1)
),
)[0]!;
const status = "Representation complete; визуальное качество ещё не принято";
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="M4.9T4 · TRAVEL TGS fail-closed evidence"
description="TRAVEL GroundSeg запущен без AOS на десяти immutable RAVNOVES00 anchors. Вход сохранён в gravity-aligned map frame; каждый eligible point получил состояние, а каждая costmap-ячейка остаётся ground, occupied, rejected или unobserved."
status={status}
statusTone="warning"
facts={[
{ label: "Конфигурация", value: `${rigLabel} RIGHT · Camera + gravity-aligned LiDAR · 10 anchors` },
{ label: "Метод", value: "TRAVEL TGS only · AOS OFF · causal rolling 1 s" },
{ label: "Evidence", value: `${result.metrics.anchorCount} anchors · ${result.metrics.costmapCellCount.toLocaleString("ru-RU")} cells/anchor · all points accounted` },
{ label: "Нагрузка", value: `Worker 006 CPU-only · GPU 0 · wrapper ${number(result.execution.wrapperElapsedSeconds, 2)} с` },
{ label: "Authority", value: "REPLAY-SIMULATED · visual/traversability/navigation/actuation OFF" },
]}
brief={{
question: "Отделяет ли готовый TRAVEL TGS опорную поверхность от неизвестной занятой геометрии достаточно чисто, чтобы заменить самодельный static-obstacle threshold pipeline?",
approach: "На десяти сложных кадрах проверяется полный gravity-aligned point set и fail-closed costmap. Зелёное — опора, красное — non-ground occupied, жёлтое — rejected/unknown, тёмное — unobserved; камера остаётся синхронным первичным контекстом.",
principalResult: `Контракт представления закрыт: ${result.metrics.anchorProfileCount}/20 профилей, ни одной потерянной eligible point, AOS и GPU отсутствуют. Process wall rolling p50/max: ${number(result.metrics.processWallRollingP50Ms, 0)}/${number(result.metrics.processWallRollingMaxMs, 0)} мс.`,
limitation: `Качество не принято: особенно проверить кадр ${worst.anchorFrameIndex + 1}, где ${number(worst.nongroundPointCount / Math.max(worst.pointCount, 1) * 100)}% rolling points помечены non-ground. Это может быть реальная боковая геометрия либо ложная блокировка поверхности.`,
}}
method={{
completeness: "complete",
executionClass: "deterministic",
pipelineId: "travel-tgs-gravity-aligned-fail-closed/v1",
components: [
{ kind: "source", name: "RAVNOVES00", version: "10 immutable anchors", role: "camera + registered map increments", identitySha256: result.source.sourcePackSha256 },
{ kind: "algorithm", name: "TRAVEL GroundSeg", version: "95dc2fbd66a343efd9060c45a5711b6307a950a4", role: "ground/nonground separation; AOS excluded", identitySha256: result.configuration.configSha256 },
{ kind: "algorithm", name: "fail-closed complement adapter", version: "v1", role: "explicit rejected points and unobserved cells", identitySha256: result.resultId.split("-").at(-1) ?? null },
],
}}
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="M4.9T4 VISUAL EVIDENCE · CAMERA + GRAVITY-ALIGNED TGS"
title="10 anchors: полный TGS point set и четырёхсостояний costmap на том же recorded timeline"
kind="recorded-replay"
resizable
>
<M49TgsFailClosedEvidence result={result} />
</LaboratoryEvidence>
)}
result={(
<LaboratoryResultSummary
title="Что уже доказано и что проверяем глазами"
status={status}
statusTone="warning"
metrics={[
{ label: "Point accounting", value: "100%", hint: "ground + non-ground + rejected = exact eligible input" },
{ label: "Anchors", value: `${result.metrics.anchorCount}/10`, hint: "current + causal rolling 1 s" },
{ label: "Costmap", value: `${result.metrics.costmapCellCount.toLocaleString("ru-RU")} cells`, hint: `${number(result.configuration.cellSizeM, 2)} м · radius ${number(result.configuration.radiusM, 0)} м` },
{ label: "Process wall rolling", value: `${number(result.metrics.processWallRollingP50Ms, 0)} / ${number(result.metrics.processWallRollingMaxMs, 0)} мс`, hint: "p50 / max · CPU process envelope, не realtime integration" },
{ label: "GPU / AOS", value: "0 / OFF", hint: "Worker 006; Frigate budget не затронут" },
]}
conclusion={{
proved: "Готовый TGS можно встроить fail-closed: исходные точки не теряются, unknown не становится free, AOS не нужен, а вычисление укладывается в лёгкий CPU-контур на этих anchors.",
notProved: "Не доказано, что красный non-ground слой не режет дорогу, траву или допустимые просветы. Нет независимой terrain truth, полного replay, realtime graph integration и модели корпуса.",
decision: "Открыть десять anchors по очереди. Если красное остаётся на реальных препятствиях и не перекрывает видимую опорную поверхность, TGS идёт в полный shadow; иначе кандидат отклоняется без ручной подгонки порогов под эти кадры.",
}}
/>
)}
/>
);
}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import {
Button,
Icon,
@@ -12,6 +12,7 @@ import {
import { ObservationTimeline } from "../../components/ObservationTimeline";
import {
LaboratoryMetricEvidenceScene,
type LaboratoryMetricCellEvidence,
type LaboratoryMetricEvidenceSceneHandle,
type LaboratoryMetricSceneMode,
} from "../../components/laboratory/LaboratoryMetricEvidenceScene";
@@ -103,6 +104,31 @@ export interface M4ReplayThreatReviewAnchor {
sourceSequence: number;
extentXyxyNormalized: readonly [number, number, number, number];
matchedAtThreshold: boolean;
statusLabel?: string;
}
export interface M4ReplayClassifiedSpatialFrame {
sourceSequence: number;
pointsMapGravityLocalXyzM: readonly (readonly [number, number, number])[];
pointClassIds: readonly (number | null)[];
cellsMapGravityLocal: readonly {
centerXyM: readonly [number, number];
zBoundsM: readonly [number | null, number | null];
state: LaboratoryMetricCellEvidence["state"];
}[];
cellSizeM: number;
classes: readonly RecordedEvidenceSemanticClass[];
palette: readonly RecordedEvidenceSemanticPaletteEntry[];
}
export interface M4ReplayClassifiedSpatialLayer {
label: string;
pointLayerLabel: string;
cellLayerLabel: string;
expectedAtSequence: boolean;
frame: M4ReplayClassifiedSpatialFrame | null;
loading: boolean;
error: string | null;
}
const EMPTY_REVIEW_ANCHORS: readonly M4ReplayThreatReviewAnchor[] = [];
@@ -115,6 +141,9 @@ export function M4ReplayThreatVisual({
reviewLabel = "Контрольные примеры M4.8R1",
timelineEndpointRoot,
evidenceLabel = "M4.6",
initialSpatialMode = null,
classifiedSpatialLayer,
onActiveSequenceChange,
}: {
resultId: string;
semantic?: M4ReplayThreatSemanticLayer;
@@ -123,9 +152,14 @@ export function M4ReplayThreatVisual({
reviewLabel?: string;
timelineEndpointRoot?: string;
evidenceLabel?: string;
initialSpatialMode?: LaboratoryMetricSceneMode | null;
classifiedSpatialLayer?: M4ReplayClassifiedSpatialLayer;
onActiveSequenceChange?: (sequence: number | null) => void;
}) {
const [mediaMode, setMediaMode] = useState<M4ThreatMediaMode | null>("video");
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(null);
const [spatialMode, setSpatialMode] = useState<LaboratoryMetricSceneMode | null>(
initialSpatialMode,
);
const [showCurrentIncrement, setShowCurrentIncrement] = useState(true);
const [showLocalSurface, setShowLocalSurface] = useState(true);
const [showRollingMap, setShowRollingMap] = useState(true);
@@ -220,6 +254,9 @@ export function M4ReplayThreatVisual({
}, [resultId]);
if (timelineFrame.activeFrame) lastFrameRef.current = timelineFrame.activeFrame;
const frame = timelineFrame.activeFrame ?? lastFrameRef.current;
useEffect(() => {
onActiveSequenceChange?.(frame?.sequence ?? null);
}, [frame?.sequence, onActiveSequenceChange]);
const lastSpatialFrameRef = useRef<{
resultId: string;
frame: M4ThreatTimelineFrame;
@@ -303,12 +340,12 @@ export function M4ReplayThreatVisual({
);
}, [frame, metadata.timeline, showStaticObstacles]);
const activeBoxes = useMemo(
() => [
() => classifiedSpatialLayer ? [] : [
...boxes(frame?.cameraProposals ?? []),
...staticObstacleBoxes,
...reviewAnchorBoxes,
],
[frame, reviewAnchorBoxes, staticObstacleBoxes],
[classifiedSpatialLayer, frame, reviewAnchorBoxes, staticObstacleBoxes],
);
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
() => semantic?.taxonomy.map((item) => ({
@@ -363,6 +400,68 @@ export function M4ReplayThreatVisual({
return status === 2 || status === 3 ? classId : null;
});
}, [semantic, semanticIntegrityError, showSpatialSemantic, spatialFrame, spatialSemanticFrame]);
const classifiedSpatialFrame = !displayingBufferedFrame
&& classifiedSpatialLayer?.frame?.sourceSequence === frame?.sequence
&& spatialFrame?.sequence === frame?.sequence
? classifiedSpatialLayer?.frame ?? null
: null;
const nominalSensorHeightM = metadata.timeline?.rig.nominalSensorHeightM ?? 0;
const mapGravityLocalSensorToBodyGround = useCallback((
point: readonly [number, number, number],
): readonly [number, number, number] => {
const basis = spatialFrame?.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],
basis[0][2] * point[0] + basis[1][2] * point[1] + basis[2][2] * point[2],
] : point;
// 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];
}, [nominalSensorHeightM, spatialFrame?.bodyFrame?.basisMapFromBody]);
const classifiedPointsBody = useMemo(
() => classifiedSpatialFrame?.pointsMapGravityLocalXyzM.map(
mapGravityLocalSensorToBodyGround,
) ?? [],
[classifiedSpatialFrame, mapGravityLocalSensorToBodyGround],
);
const classifiedCellsBody = useMemo<readonly LaboratoryMetricCellEvidence[]>(
() => classifiedSpatialFrame?.cellsMapGravityLocal.map((cell) => {
const body = mapGravityLocalSensorToBodyGround([
cell.centerXyM[0],
cell.centerXyM[1],
0,
]);
const [minimumSensorRelativeZ, maximumSensorRelativeZ] = cell.zBoundsM;
return {
centerBodyXyM: [body[0], body[1]],
zBoundsM: [
minimumSensorRelativeZ === null
? null
: minimumSensorRelativeZ + nominalSensorHeightM,
maximumSensorRelativeZ === null
? null
: maximumSensorRelativeZ + nominalSensorHeightM,
],
state: cell.state,
};
}) ?? [],
[classifiedSpatialFrame, 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 sceneObstacles = useMemo(() => spatialFrame?.metricObstacles.map((obstacle) => ({
id: obstacle.componentId,
decision: obstacle.assessment.decision,
@@ -522,7 +621,32 @@ export function M4ReplayThreatVisual({
</div>
) : null;
const spatialLayerControls = (
const spatialLayerControls = classifiedSpatialLayer ? (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
aria-label={`Слои ${classifiedSpatialLayer.label}`}
>
<Button
size="compact"
shape="pill"
variant={showCurrentIncrement ? "primary" : "secondary"}
aria-pressed={showCurrentIncrement}
onClick={() => setShowCurrentIncrement((visible) => !visible)}
>
{classifiedSpatialLayer.pointLayerLabel}
</Button>
<Button
size="compact"
shape="pill"
variant={showRollingMap ? "primary" : "secondary"}
aria-pressed={showRollingMap}
onClick={() => setShowRollingMap((visible) => !visible)}
>
{classifiedSpatialLayer.cellLayerLabel}
</Button>
</div>
) : (
<div
className="m4-replay-threat-visual__pane-layer-controls"
role="group"
@@ -626,7 +750,7 @@ export function M4ReplayThreatVisual({
value={String(selectedReviewAnchorIndex)}
options={reviewAnchors.map((anchor, index) => ({
value: String(index),
label: `${index + 1}/${reviewAnchors.length} · кадр ${anchor.sourceSequence + 1} · ${anchor.matchedAtThreshold ? "покрыт" : "пропуск"}`,
label: `${index + 1}/${reviewAnchors.length} · кадр ${anchor.sourceSequence + 1} · ${anchor.statusLabel ?? (anchor.matchedAtThreshold ? "покрыт" : "пропуск")}`,
}))}
variant="split"
menuWidth="anchor"
@@ -671,39 +795,48 @@ export function M4ReplayThreatVisual({
</div>
<div>
<span>Spatial evidence</span>
<strong>
{currentIncrementObstacles.length} current · {rollingMapObstacles.length} rolling
{metadata.timeline.occupancyProvenanceDelivery
? ` · ${lowStepObstacles.length} low-step`
: ""}
</strong>
<small>
{spatialFrame
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
: "квалифицированный spatial frame ещё не получен"}
{frame.worldStateAvailable
? " · world-state delivered"
: ` · world-state gap (${frame.terminalOutcome})`}
{accumulatedCameraPoints
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
: pointCloudOverlay
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
: showMediaPoints && cameraPointOverlay.error
? " · накопленное camera cloud недоступно"
: ""}
{semantic && spatialSemanticFrame
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
: semantic ? " · semantic buffer" : ""}
</small>
<strong>{classifiedSpatialLayer
? classifiedSpatialFrame
? `${classifiedSpatialFrame.pointsMapGravityLocalXyzM.length.toLocaleString("ru-RU")} TGS points · ${classifiedSpatialFrame.cellsMapGravityLocal.length.toLocaleString("ru-RU")} cells`
: "TGS spatial buffer"
: `${currentIncrementObstacles.length} current · ${rollingMapObstacles.length} rolling${metadata.timeline.occupancyProvenanceDelivery ? ` · ${lowStepObstacles.length} low-step` : ""}`}</strong>
<small>{classifiedSpatialLayer
? classifiedSpatialFrame
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
: classifiedSpatialLayer.error ?? `Открываем ${classifiedSpatialLayer.label}`
: (
<>
{spatialFrame
? `${spatialFrame.pointCloudSampleCount}/${spatialFrame.pointCloudSourceCount} exact · ${localSurface.pointsBodyXyzM.length} local SLAM / ${localSurface.sourceFrameCount} frames`
: "квалифицированный spatial frame ещё не получен"}
{frame.worldStateAvailable
? " · world-state delivered"
: ` · world-state gap (${frame.terminalOutcome})`}
{accumulatedCameraPoints
? ` · camera points ${accumulatedCameraPoints.sampleCount}/${accumulatedCameraPoints.projectedPointCount} · causal ${accumulatedCameraPoints.windowSeconds.toFixed(1)} с / ${accumulatedCameraPoints.sourceFrameCount} frames`
: pointCloudOverlay
? ` · camera points ${frame.cameraProjectedSampleCount}/${frame.cameraProjectedPointCount} exact-current · накопление загружается`
: showMediaPoints && cameraPointOverlay.error
? " · накопленное camera cloud недоступно"
: ""}
{semantic && spatialSemanticFrame
? ` · semantic L ${spatialSemanticFrame.counts.labeled} · A ${spatialSemanticFrame.counts.ambiguous} · U ${spatialSemanticFrame.counts.unprojected} · Ø ${spatialSemanticFrame.counts.absent}`
: semantic ? " · semantic buffer" : ""}
</>
)}</small>
</div>
<div>
<span>Virtual corridor</span>
<strong>
{spatialFrame?.decisionCounts.threat ?? 0} threat · nearest {nearest === null ? "—" : `${nearest.toFixed(2)} м`}
</strong>
<small>
{metadata.timeline.corridor.forwardLengthM} м · body {metadata.timeline.rig.lengthM}×{metadata.timeline.rig.widthM} м · REPLAY-SIMULATED
</small>
<span>{classifiedSpatialLayer ? "TGS fail-closed" : "Virtual corridor"}</span>
<strong>{classifiedSpatialLayer
? classifiedSpatialFrame
? `${classifiedCellCounts.occupied} occupied · ${classifiedCellCounts.rejected} rejected · ${classifiedCellCounts.unobserved} unobserved`
: classifiedSpatialLayer.loading || displayingBufferedFrame ? "loading" : "unavailable"
: `${spatialFrame?.decisionCounts.threat ?? 0} threat · nearest ${nearest === null ? "—" : `${nearest.toFixed(2)} м`}`}</strong>
<small>{classifiedSpatialLayer
? classifiedSpatialFrame
? `${classifiedCellCounts.ground} ground-support · visual review only · navigation authority OFF`
: "visual review only · navigation authority OFF"
: `${metadata.timeline.corridor.forwardLengthM} м · body ${metadata.timeline.rig.lengthM}×${metadata.timeline.rig.widthM} м · REPLAY-SIMULATED`}</small>
</div>
</div>
) : undefined;
@@ -802,26 +935,50 @@ export function M4ReplayThreatVisual({
</div>
</div>
) : null}
{spatialFrame ? (
{spatialFrame && (!classifiedSpatialLayer || classifiedSpatialFrame) ? (
<LaboratoryMetricEvidenceScene
ref={metricSceneRef}
pointCloudBodyXyzM={spatialFrame.pointCloudBodyXyzM}
localSurfaceBodyXyzM={localSurface.pointsBodyXyzM}
obstacles={sceneObstacles}
pointCloudBodyXyzM={classifiedSpatialFrame
? classifiedPointsBody
: spatialFrame.pointCloudBodyXyzM}
localSurfaceBodyXyzM={classifiedSpatialFrame ? [] : localSurface.pointsBodyXyzM}
obstacles={classifiedSpatialFrame ? [] : sceneObstacles}
rig={timeline.rig}
corridor={timeline.corridor}
occupiedVoxelSizeM={timeline.occupiedVoxelSizeM}
occupiedVoxelSizeM={classifiedSpatialFrame?.cellSizeM ?? timeline.occupiedVoxelSizeM}
mode={spatialMode}
label={`${evidenceLabel} exact current increment, bounded local SLAM surface and rolling occupancy`}
showCurrentIncrement={showCurrentIncrement}
showLocalSurface={showLocalSurface}
showLocalSurface={classifiedSpatialFrame ? false : showLocalSurface}
showRollingMap={showRollingMap}
showLowStep={showLowStep}
pointSemanticClassIds={alignedSemanticPointIds}
semanticClasses={semanticClasses}
semanticPalette={semanticPalette}
showLowStep={classifiedSpatialFrame ? false : showLowStep}
pointSemanticClassIds={classifiedSpatialFrame
? classifiedSpatialFrame.pointClassIds
: alignedSemanticPointIds}
semanticClasses={classifiedSpatialFrame
? classifiedSpatialFrame.classes
: semanticClasses}
semanticPalette={classifiedSpatialFrame
? classifiedSpatialFrame.palette
: semanticPalette}
classifiedCells={classifiedCellsBody}
classifiedCellSizeM={classifiedSpatialFrame?.cellSizeM}
showClassifiedCells={showRollingMap}
/>
) : null}
{classifiedSpatialLayer && !classifiedSpatialFrame ? (
<div className="l3-visual-audit__state" role={classifiedSpatialLayer.error ? "alert" : "status"}>
{classifiedSpatialLayer.loading || displayingBufferedFrame
? <span className="busy-indicator" aria-hidden="true" />
: <Icon name="alert" size={18} />}
<span>{classifiedSpatialLayer.error
?? (classifiedSpatialLayer.loading || displayingBufferedFrame
? `Открываем ${classifiedSpatialLayer.label}`
: classifiedSpatialLayer.expectedAtSequence
? `Открываем ${classifiedSpatialLayer.label}`
: `${classifiedSpatialLayer.label} рассчитан только на 10 контрольных кадров.`)}</span>
</div>
) : null}
{frame && !frame.spatialAvailable ? (
<div className="m4-replay-threat-visual__pane-status" role="status">
{spatialFrame
@@ -105,6 +105,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
experimentName: "RF-DETR native risk review and temporal identity",
variantName: "M4.8Q · native raw KB4 review · quality not adjudicated",
},
"m49-tgs-fail-closed-evidence": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + gravity-aligned LiDAR`,
experimentId: "m49-tgs-fail-closed-evidence",
experimentName: "TRAVEL TGS fail-closed traversability evidence",
variantName: "M4.9T4 · 10 anchors · causal rolling 1 s · AOS OFF",
},
"m47-reference-graph-shadow": {
profileId: "rig-dual-evidence-virtual-corridor-v1",
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Camera + LiDAR dual evidence`,
@@ -25,6 +25,7 @@ function mergeResults(
m48r3StaticOccupancy: next.m48r3StaticOccupancy ?? current.m48r3StaticOccupancy,
m48s: next.m48s ?? current.m48s,
m48t: next.m48t ?? current.m48t,
m49Tgs: next.m49Tgs ?? current.m49Tgs,
m4Threat: next.m4Threat ?? current.m4Threat,
l3: next.l3 ?? current.l3,
l31: next.l31 ?? current.l31,
@@ -124,6 +125,7 @@ export function useAdvancedLaboratoryCatalog({
"m48-small-static-passage-regression",
"m48-static-occupancy-qualification",
"m48r3-static-occupancy-shadow",
"m49-tgs-fail-closed-evidence",
].includes(selectedWorkId)
&& !indexedResultId
) return;
@@ -7,6 +7,7 @@ let server;
let fetchAdvancedLaboratoryResults;
let fetchAdvancedLaboratoryIndex;
let fetchAdvancedLaboratoryResult;
let fetchM49TgsAnchorSpatial;
let AdvancedLaboratoryContractError;
let buildLaboratoryCatalog;
let buildLaboratoryProfiles;
@@ -836,6 +837,77 @@ function e40() {
};
}
function m49View() {
const anchors = [171, 306, 368, 402, 450, 509, 525, 744, 1122, 1856];
return {
schema_version: "missioncore.m49-tgs-fail-closed-view/v1",
result_id: `m49-tgs-fail-closed-${"2".repeat(64)}`,
created_at_utc: "2026-08-26T17:09:43Z",
source: {
source_id: "RAVNOVES00",
source_session_id: "20260720T065719Z_viewer_live",
source_pack_sha256: "3".repeat(64),
linked_visual_result_id: `m4-threat-replay-${"4".repeat(64)}`,
anchor_frame_indices: anchors,
},
configuration: {
profile_id: "m49-ravnoves00-tgs-fail-closed-evidence/v1",
config_sha256: "5".repeat(64),
coordinate_frame: "map-gravity-local",
primary_profile: "causal_rolling_1s",
cell_size_m: 0.45,
radius_m: 12,
},
execution: {
worker: "Worker 006",
device: "cpu",
gpu_used: false,
wrapper_elapsed_seconds: 12.1,
},
metrics: {
anchor_count: 10,
anchor_profile_count: 20,
all_eligible_points_accounted: true,
costmap_cell_count: 2244,
process_wall_current_p50_ms: 20,
process_wall_current_max_ms: 30,
process_wall_rolling_p50_ms: 30,
process_wall_rolling_max_ms: 40,
process_max_rss_kib: 9292,
primary: anchors.map((anchor, slot) => ({
anchor_frame_index: anchor,
slot,
profile_id: "causal_rolling_1s",
point_count: 3,
ground_point_count: 1,
nonground_point_count: 1,
rejected_point_count: 1,
ground_cell_count: 1,
nonground_cell_count: 1,
rejected_cell_count: 1,
unobserved_cell_count: 2241,
all_points_accounted: true,
})),
},
acceptance: {
representation_complete: true,
all_points_accounted: true,
aos_absent: true,
gpu_absent: true,
visual_quality_accepted: false,
traversability_accepted: false,
},
decision: {
state: "visual-review-required",
candidate_retained: true,
next_action: "Review the ten anchors.",
},
limitations: ["bounded diagnostic evidence"],
ground_truth: false,
access: "read-only",
};
}
before(async () => {
server = await createServer({
appType: "custom",
@@ -850,6 +922,9 @@ before(async () => {
fetchAdvancedLaboratoryIndex,
fetchAdvancedLaboratoryResult,
} = await server.ssrLoadModule("/src/core/laboratory/advancedIndex.ts"));
({ fetchM49TgsAnchorSpatial } = await server.ssrLoadModule(
"/src/core/laboratory/m49TgsFailClosed.ts",
));
({
buildLaboratoryCatalog,
buildLaboratoryProfiles,
@@ -1596,6 +1671,62 @@ test("selected advanced LAB fetches only its own strict catalog", async () => {
]);
});
test("selected M49 LAB preserves the sealed fail-closed contract", async () => {
const payload = m49View();
const requests = [];
const decoded = await fetchAdvancedLaboratoryResult(
"m49-tgs-fail-closed-evidence",
{
resultId: payload.result_id,
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
return new Response(JSON.stringify(payload), { status: 200 });
},
},
);
assert.equal(decoded.m49Tgs.metrics.anchorCount, 10);
assert.equal(decoded.m49Tgs.metrics.costmapCellCount, 2244);
assert.equal(decoded.m49Tgs.acceptance.visualQualityAccepted, false);
assert.deepEqual(requests, [{
input: `/api/v1/laboratory/m49/tgs-fail-closed/${payload.result_id}`,
method: "GET",
}]);
});
test("M49 anchor fetch keeps every point and all four costmap states", async () => {
const resultId = m49View().result_id;
const centers = Array.from({ length: 2244 }, (_, index) => [index * 0.45, 0]);
const states = Array.from({ length: 2244 }, (_, index) => index % 4);
const zBounds = states.map((state) => state === 0 ? [null, null] : [0, 0.2]);
const decoded = await fetchM49TgsAnchorSpatial(resultId, 171, "causal_rolling_1s", {
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.m49-tgs-anchor-spatial/v1",
result_id: resultId,
anchor_frame_index: 171,
source_sequence: 171,
profile: "causal_rolling_1s",
coordinate_frame: "map-gravity-local",
points_xyz_m: [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
point_states: [1, 2, 3],
costmap: {
cell_size_m: 0.45,
radius_m: 12,
centers_xy_m: centers,
states,
z_bounds_m: zBounds,
},
all_points_accounted: true,
aos_used: false,
access: "read-only",
}), { status: 200 }),
});
assert.deepEqual(decoded.pointStates, [1, 2, 3]);
assert.equal(decoded.costmap.states.length, 2244);
assert.deepEqual(new Set(decoded.costmap.states), new Set([0, 1, 2, 3]));
});
test("keeps valid LAB catalogs available when one transport endpoint fails", async () => {
const decoded = await fetchAdvancedLaboratoryResults({
fetcher: async (input) => {
@@ -799,7 +799,23 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
assert.match(metricScene, /OrbitControls/);
assert.match(visual, /LOCAL SLAM/);
assert.match(visual, /showLocalSurface/);
assert.match(visual, /pointCloudBodyXyzM=\{spatialFrame\.pointCloudBodyXyzM\}/);
assert.match(
visual,
/pointCloudBodyXyzM=\{classifiedSpatialFrame[\s\S]*\? classifiedPointsBody[\s\S]*: spatialFrame\.pointCloudBodyXyzM\}/,
);
assert.match(
visual,
/const classifiedSpatialFrame = !displayingBufferedFrame[\s\S]*classifiedSpatialLayer\?\.frame\?\.sourceSequence === frame\?\.sequence[\s\S]*spatialFrame\?\.sequence === frame\?\.sequence/,
);
assert.match(
visual,
/mapGravityLocalSensorToBodyGround[\s\S]*rotated\[2\] \+ nominalSensorHeightM/,
);
assert.match(
visual,
/zBoundsM: \[[\s\S]*minimumSensorRelativeZ \+ nominalSensorHeightM[\s\S]*maximumSensorRelativeZ \+ nominalSensorHeightM/,
);
assert.match(visual, /classifiedCells=\{classifiedCellsBody\}/);
assert.match(metricScene, /Локальная SLAM-поверхность/);
assert.match(visual, /showJumpToEnd=\{false\}/);
assert.doesNotMatch(visual, /Назад на 5 секунд/);