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 секунд/);
@@ -0,0 +1,10 @@
{
"schema_version": "missioncore.laboratory-evidence-definition/v1",
"work_id": "m49-tgs-fail-closed-evidence",
"evidence": {
"runtime_relative_root": "m49/tgs-fail-closed-results",
"result_id_prefix": "m49-tgs-fail-closed",
"document_name": "manifest.json",
"schema_version": "missioncore.m49-tgs-fail-closed-result/v1"
}
}
+14
View File
@@ -162,6 +162,20 @@
"run": "missioncore.laboratory-run/v1",
"evidence": "missioncore.m48t-risk-quality-temporal-lab/v1"
}
},
{
"work_id": "m49-tgs-fail-closed-evidence",
"lifecycle": "experimental",
"isolation": "bounded-adapter",
"adapter_id": "experimental.m49-tgs-fail-closed-evidence/v1",
"input_roles": ["repository_root"],
"contracts": {
"source": "missioncore.m49-tgs-worker-evidence/v1",
"provider": "missioncore.travel-tgs-ground-segmentation/v1",
"graph": "missioncore.m49-fail-closed-costmap-evidence/v1",
"run": "missioncore.laboratory-run/v1",
"evidence": "missioncore.m49-tgs-fail-closed-result/v1"
}
}
],
"legacy_work_ids": [
+7
View File
@@ -267,6 +267,13 @@
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "m49-tgs-fail-closed-evidence",
"evidence_id": "m49-tgs-fail-closed-9d5cb089bb5cc23f829acb47eda52eaa886db3b60f8fa7d57e02e27f642e837b",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
}
]
}
@@ -326,7 +326,11 @@ invocation. It emits a deterministic `0.45 m`, `12 m` local evidence grid while
preserving unobserved cells. See
[`experiments/perception/M49_TGS_FAIL_CLOSED_EVIDENCE_2026-08-26.md`](../experiments/perception/M49_TGS_FAIL_CLOSED_EVIDENCE_2026-08-26.md).
Representation/accounting is accepted for the visual gate; obstacle and
traversability quality are not yet accepted.
traversability quality are not yet accepted. The sealed result is now published
in the canonical LAB as
`m49-tgs-fail-closed-9d5cb089bb5cc23f829acb47eda52eaa886db3b60f8fa7d57e02e27f642e837b`,
linked to the current M4 v3 recorded timeline. Publication adds no navigation
or actuation authority.
### T4 — Candidate C occupancy/ESDF probe
@@ -345,12 +349,14 @@ traversability quality are not yet accepted.
## Immediate next action
Import the sealed gravity-aligned TGS evidence pack into one laboratory review
surface and review the ten anchors in metric 3D/costmap space. Preserve
Review the published gravity-aligned TGS evidence pack across all ten anchors
in metric 3D/costmap space. Preserve
`GROUND_SUPPORT`, `NONGROUND_OCCUPIED`, `UNKNOWN_REJECTED` and `UNOBSERVED` as
separate products; do not use AOS or infer free cells from absent
republication. Add diagnostic camera projection only after the metric evidence
is accepted. Candidate A T2 replay remains blocked by the failed unmodified T1
gate. No LOW-STEP tuning, new object model, camera resize, fisheye
rectification, manual dataset or parallel heavy Worker job is authorized by
this decision.
is accepted. If occupied evidence carpets the route, soft traversable
vegetation or usable gaps, reject TGS without tuning it against the review
anchors; otherwise advance it to a full recorded-source shadow. Candidate A T2
replay remains blocked by the failed unmodified T1 gate. No LOW-STEP tuning,
new object model, camera resize, fisheye rectification, manual dataset or
parallel heavy Worker job is authorized by this decision.
@@ -1,7 +1,8 @@
# M4.9 gravity-aligned TGS fail-closed evidence — 2026-08-26
Status: **evidence adapter completed**; visual obstacle/traversability quality,
full-source realtime, navigation and actuation remain disabled
Status: **evidence adapter and canonical LAB publication completed**; visual
obstacle/traversability quality, full-source realtime, navigation and actuation
remain disabled
## Decision
@@ -13,7 +14,9 @@ accounts for every point inside TRAVEL's declared `180 m` processing range.
This closes the representation and accounting gate. It does not accept visual
quality. Several anchors still contain a high non-ground share, so the next
gate is a 3D/costmap review of the sealed output rather than threshold tuning or
camera boxes.
camera boxes. The metric output is now available in the canonical LAB on port
`8000` as an operator-review surface; publication does not upgrade it to an
accepted terrain provider.
## Frozen identity
@@ -125,15 +128,73 @@ Worker root:
| `inputs/input-manifest.json` | `13,357` | `7a22e61606e1fa9ab61d47417f941b590a68b29e4529d4c290992ce7eb89a642` |
| `tgs-timing.tsv` | `638` | `a8f439996205286335ac392b37f51b54056d04e9cb07e03648c52c1e8f2f6ae8` |
## Canonical LAB publication
- LAB result:
`m49-tgs-fail-closed-9d5cb089bb5cc23f829acb47eda52eaa886db3b60f8fa7d57e02e27f642e837b`;
- linked current visual timeline:
`m4-threat-replay-2a953c5f27f2a5b1dddc5c658c1de2c323d7796084a099c024987a1da03aa324`;
- source LiDAR pack SHA-256 remains
`0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944`;
- the earlier immutable seal beginning `200b57cc` is superseded only because it
linked the retired M4 v2 visual profile, whose timeline is incompatible with
the current v3 API. No Worker evidence byte or TGS result was changed.
The LAB reuses the canonical recorded-realtime camera and spatial instrument.
It exposes the ten anchors, the full classified TGS point set, the four-state
costmap, `VIDEO/CAMERA`, `3D/PLAN`, independent point/costmap visibility and
exact per-anchor counts. Camera proposal boxes, AOS clusters, future frames and
navigation authority are absent. Browser acceptance confirmed that every
operator control is independent and that a loading anchor cannot temporarily
display unrelated M4 obstacle statistics.
The published TGS coordinates are intentionally sensor-origin
`map-gravity-local`: the ground is therefore approximately `-1.25 m` below the
current LiDAR. The canonical LAB presentation converts these immutable values
to the scene's body-ground origin by adding the rig's sealed
`nominal_sensor_height_m = 1.25` after the gravity-to-body rotation. The same
offset is applied to point heights and costmap z-bounds; x/y evidence and the
sealed Worker result remain unchanged. A regression check across all ten
anchors puts the median displayed ground points in `-0.299…+0.135 m` and the
median ground-cell centres in `-0.308…+0.237 m`. On anchor `1856`, where the
presentation defect was reported, those medians are `+0.004 m` and `-0.063 m`.
The residual variation is measured terrain/pose variation, not the previous
systematic `1.25 m` lift.
## Preliminary corridor triage
The published evidence was also projected into the frozen virtual body
corridor (`-0.5…8.0 m` longitudinal, `±0.5 m` lateral) without changing the
sealed result. The table counts costmap cell centres inside that corridor:
| Anchor | Current occupied | Rolling occupied | Rolling unobserved | Preliminary reading |
| ---: | ---: | ---: | ---: | --- |
| `171` | `0` | `8` | `6` | unresolved rolling band at `5.63…7.90 m` |
| `306` | `0` | `0` | `12` | no corridor obstruction |
| `368` | `1` | `1` | `16` | compact occupied support at `6.34 m` |
| `402` | `0` | `1` | `10` | compact occupied support at `1.89 m` |
| `450` | `0` | `0` | `8` | no corridor obstruction |
| `509` | `0` | `0` | `7` | no corridor obstruction |
| `525` | `0` | `0` | `9` | no corridor obstruction |
| `744` | `0` | `3` | `25` | weakly observed; requires explicit review |
| `1122` | `0` | `2` | `24` | rolling recovers compact obstacle support |
| `1856` | `1` | `1` | `13` | compact occupied support at `4.14 m` |
This is a diagnostic, not ground truth. It shows why the causal window cannot
simply be removed: anchor `1122` is entirely unobserved in the current-only
corridor but gains ground and occupied evidence in rolling. It also identifies
the opposing risk: at anchor `171`, rolling adds eight occupied cells where the
camera/PLAN comparison appears to show an open route apart from side geometry.
Anchor `744` remains too weakly observed for acceptance. These two anchors keep
the visual gate open even though the remaining frames do not show a continuous
occupied carpet.
## Next gate
Import the sealed `evidence.npz` into one laboratory review surface with:
1. 3D points colored by the four evidence states;
2. top-down `0.45 m` costmap cells in the same local coordinate frame;
3. anchor selection and exact counts from `result.json`;
4. no AOS clusters, camera boxes, future frames or physical authority.
The operator review must focus on the mandatory obstacle/gap cases and on the
large non-ground populations listed above. Camera projection follows only if
the metric evidence is accepted.
Review the published ten anchors in both metric `3D` and `PLAN`. The operator
must focus on the mandatory obstacle/gap cases and the large non-ground
populations listed above. Accept TGS for a full recorded-source shadow only if
red non-ground evidence remains on real geometry without carpeting the visible
route, soft traversable vegetation or usable gaps. Otherwise reject this
candidate without tuning thresholds against these ten anchors. Camera
projection follows only if the metric evidence is accepted.
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Publish the verified Worker TGS pack as immutable Mission Core LAB evidence."""
from __future__ import annotations
import argparse
from pathlib import Path
from k1link.laboratory.m49_tgs_fail_closed import (
M49TgsFailClosedError,
seal_m49_tgs_fail_closed,
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source-root", type=Path, required=True)
parser.add_argument("--destination-root", type=Path, required=True)
parser.add_argument("--profile", type=Path, required=True)
parser.add_argument("--linked-visual-result-id", required=True)
arguments = parser.parse_args()
try:
result = seal_m49_tgs_fail_closed(
source_root=arguments.source_root,
destination_root=arguments.destination_root,
profile_path=arguments.profile,
linked_visual_result_id=arguments.linked_visual_result_id,
)
except (M49TgsFailClosedError, OSError, ValueError) as exc:
parser.error(str(exc))
print(result.result_id)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,440 @@
"""Seal and verify bounded gravity-aligned TRAVEL TGS evidence."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import tempfile
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Final
M49_TGS_RESULT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-result/v1"
M49_TGS_REPORT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-report/v1"
M49_TGS_WORKER_RESULT_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-evidence-result/v1"
M49_TGS_PREFIX: Final = "m49-tgs-fail-closed-"
M49_TGS_PROFILE_ID: Final = "m49-ravnoves00-tgs-fail-closed-evidence/v1"
M49_TGS_ANCHORS: Final = (171, 306, 368, 402, 450, 509, 525, 744, 1122, 1856)
M49_TGS_PROFILES: Final = ("current_increment", "causal_rolling_1s")
_MAX_JSON_BYTES: Final = 1024 * 1024
_HASH_CHUNK_BYTES: Final = 1024 * 1024
class M49TgsFailClosedError(RuntimeError):
"""The TGS evidence pack is unavailable or failed its immutable contract."""
@dataclass(frozen=True, slots=True)
class M49TgsFailClosedResult:
result_id: str
root: Path
manifest: dict[str, Any]
report: dict[str, Any]
@property
def evidence_path(self) -> Path:
return self.root / "evidence.npz"
def seal_m49_tgs_fail_closed(
*,
source_root: Path,
destination_root: Path,
profile_path: Path,
linked_visual_result_id: str,
created_at_utc: str | None = None,
) -> M49TgsFailClosedResult:
source = _real_directory(source_root, "M49 Worker evidence")
destination = destination_root.expanduser().absolute()
destination.mkdir(parents=True, exist_ok=True)
if destination.is_symlink():
raise M49TgsFailClosedError("M49 destination must not be a symlink")
profile = _json_file(profile_path, "M49 profile")
worker_result = _json_file(source / "result.json", "M49 Worker result")
worker_summary = _json_file(source / "worker-summary.json", "M49 Worker summary")
input_manifest = _json_file(source / "input-manifest.json", "M49 input manifest")
timing = _timing_metrics(source / "tgs-timing.tsv")
_validate_source(profile, worker_result, worker_summary, input_manifest, source)
if (
not linked_visual_result_id.startswith("m4-threat-replay-")
or len(linked_visual_result_id) != len("m4-threat-replay-") + 64
):
raise M49TgsFailClosedError("M49 linked visual result is invalid")
evidence_sha = _file_sha256(source / "evidence.npz")
identity = {
"schema_version": M49_TGS_RESULT_SCHEMA,
"source": {
"source_id": "RAVNOVES00",
"source_session_id": "20260720T065719Z_viewer_live",
"source_pack_sha256": worker_result["source_pack_sha256"],
"input_manifest_sha256": worker_result["input_manifest_sha256"],
"linked_visual_result_id": linked_visual_result_id,
"anchor_frame_indices": list(M49_TGS_ANCHORS),
},
"configuration": {
"profile_id": M49_TGS_PROFILE_ID,
"config_sha256": worker_result["config_sha256"],
"coordinate_frame": "map-gravity-local",
"primary_profile": "causal_rolling_1s",
"cell_size_m": worker_result["costmap"]["cell_size_m"],
"radius_m": worker_result["costmap"]["radius_m"],
},
"method": {
"execution_class": "deterministic",
"pipeline_id": "travel-tgs-gravity-aligned-fail-closed/v1",
"travel_revision": profile["source"]["travel_revision"],
"aos_used": False,
"missing_support_means_free": False,
"eligible_point_accounting": "exact-multiset-complement",
},
"evidence": {
"sha256": evidence_sha,
"byte_length": (source / "evidence.npz").stat().st_size,
"state_codes": profile["state_codes"],
},
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
"visual_quality_accepted": False,
},
}
identity_sha256 = _canonical_sha256(identity)
result_id = f"{M49_TGS_PREFIX}{identity_sha256}"
target = destination / result_id
if target.exists():
return read_m49_tgs_fail_closed(target)
created = created_at_utc or datetime.now(tz=UTC).isoformat().replace("+00:00", "Z")
anchors = worker_result["anchors"]
primary = [row for row in anchors if row["profile_id"] == "causal_rolling_1s"]
report = {
"schema_version": M49_TGS_REPORT_SCHEMA,
"result_id": result_id,
"created_at_utc": created,
"source": identity["source"],
"configuration": {
**identity["configuration"],
"state_priority": profile["costmap"]["state_priority"],
"tgs": profile["tgs"],
},
"method": {
**identity["method"],
"components": [
{
"kind": "algorithm",
"name": "TRAVEL GroundSeg",
"version": profile["source"]["travel_revision"],
"role": "gravity-aligned ground/nonground separation",
},
{
"kind": "algorithm",
"name": "fail-closed complement adapter",
"version": "v1",
"role": "retain rejected points and explicit unobserved cells",
},
{
"kind": "runtime",
"name": "Worker 006 CPU qualification",
"version": worker_summary["code_revision"],
"role": "20 bounded TGS invocations without GPU",
},
],
},
"execution": {
"worker": "Worker 006",
"device": "cpu",
"gpu_used": False,
"wrapper_elapsed_seconds": worker_summary["wall_seconds"],
"canonical_triton_id": worker_summary["canonical_triton_id"],
"canonical_triton_health": worker_summary["canonical_triton_health"],
"free_memory_gib_before": worker_summary["free_memory_gib_before"],
},
"metrics": {
"anchor_count": len(M49_TGS_ANCHORS),
"anchor_profile_count": len(anchors),
"all_eligible_points_accounted": True,
"primary_profile": "causal_rolling_1s",
"primary": primary,
"costmap_cell_count": worker_result["costmap"]["cell_count"],
"process_wall_current_p50_ms": timing["current_increment"]["p50_ms"],
"process_wall_current_max_ms": timing["current_increment"]["max_ms"],
"process_wall_rolling_p50_ms": timing["causal_rolling_1s"]["p50_ms"],
"process_wall_rolling_max_ms": timing["causal_rolling_1s"]["max_ms"],
"process_max_rss_kib": timing["max_rss_kib"],
},
"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 exact gravity-aligned points and fail-closed costmap "
"on the ten immutable anchors."
),
},
"limitations": [
"The ten anchors are bounded diagnostic evidence, not a full 4,489-frame replay.",
"No independent terrain or traversability truth is available.",
"No vehicle envelope exists, so occupied cells do not grant or deny physical passage.",
"Visual quality, realtime integration, navigation and actuation remain unaccepted.",
],
"authority": {
"mode": "replay-simulated",
"commands_enabled": False,
"navigation_or_safety_accepted": False,
"visual_quality_accepted": False,
},
"visual_review": {
"instrument": "m4-canonical-reference-graph",
"linked_visual_result_id": linked_visual_result_id,
"anchors": list(M49_TGS_ANCHORS),
"profiles": list(M49_TGS_PROFILES),
"default_profile": "causal_rolling_1s",
"point_states": profile["state_codes"],
},
}
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-", dir=destination) as raw:
staging = Path(raw) / result_id
staging.mkdir()
for name in (
"evidence.npz",
"worker-summary.json",
"input-manifest.json",
"tgs-timing.tsv",
):
shutil.copyfile(source / name, staging / name)
_write_json(staging / "report.json", report)
artifacts = [
_artifact(staging / "report.json", "report", M49_TGS_REPORT_SCHEMA, "application/json"),
_artifact(
staging / "evidence.npz", "visual-spatial-evidence", None, "application/x-npz"
),
_artifact(staging / "worker-summary.json", "runtime-summary", None, "application/json"),
_artifact(staging / "input-manifest.json", "source-manifest", None, "application/json"),
_artifact(
staging / "tgs-timing.tsv", "runtime-timing", None, "text/tab-separated-values"
),
]
manifest = {
"schema_version": M49_TGS_RESULT_SCHEMA,
"result_id": result_id,
"created_at_utc": created,
"identity_sha256": identity_sha256,
"identity": identity,
"artifacts": artifacts,
"authority": report["authority"],
"ground_truth": False,
}
_write_json(staging / "manifest.json", manifest)
os.replace(staging, target)
return read_m49_tgs_fail_closed(target)
def read_m49_tgs_fail_closed(root: Path) -> M49TgsFailClosedResult:
candidate = _real_directory(root, "M49 result")
if not candidate.name.startswith(M49_TGS_PREFIX):
raise M49TgsFailClosedError("M49 result identity is invalid")
manifest = _json_file(candidate / "manifest.json", "M49 manifest")
report = _json_file(candidate / "report.json", "M49 report")
if (
manifest.get("schema_version") != M49_TGS_RESULT_SCHEMA
or manifest.get("result_id") != candidate.name
or report.get("schema_version") != M49_TGS_REPORT_SCHEMA
or report.get("result_id") != candidate.name
):
raise M49TgsFailClosedError("M49 result contract changed")
identity = manifest.get("identity")
identity_sha = manifest.get("identity_sha256")
if (
not isinstance(identity, dict)
or not isinstance(identity_sha, str)
or _canonical_sha256(identity) != identity_sha
or candidate.name != f"{M49_TGS_PREFIX}{identity_sha}"
):
raise M49TgsFailClosedError("M49 identity proof changed")
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list) or len(artifacts) != 5:
raise M49TgsFailClosedError("M49 artifact manifest changed")
for item in artifacts:
if not isinstance(item, dict):
raise M49TgsFailClosedError("M49 artifact descriptor changed")
path = candidate / str(item.get("path", ""))
if (
path.is_symlink()
or not path.is_file()
or path.parent != candidate
or path.stat().st_size != item.get("byte_length")
or _file_sha256(path) != item.get("sha256")
):
raise M49TgsFailClosedError("M49 artifact proof changed")
return M49TgsFailClosedResult(candidate.name, candidate, manifest, report)
def _validate_source(
profile: dict[str, Any],
worker_result: dict[str, Any],
worker_summary: dict[str, Any],
input_manifest: dict[str, Any],
source: Path,
) -> None:
if (
profile.get("schema_version") != "missioncore.m49-tgs-fail-closed-evidence-profile/v1"
or profile.get("profile_id") != M49_TGS_PROFILE_ID
or tuple(profile.get("anchors", ())) != M49_TGS_ANCHORS
or profile.get("invariants", {}).get("aos_allowed") is not False
or profile.get("invariants", {}).get("missing_support_means_free") is not False
):
raise M49TgsFailClosedError("M49 profile changed")
if (
worker_result.get("schema_version") != M49_TGS_WORKER_RESULT_SCHEMA
or worker_result.get("status") != "passed"
or worker_result.get("summary", {}).get("aos_used") is not False
or worker_result.get("summary", {}).get("all_eligible_points_accounted") is not True
or worker_result.get("summary", {}).get("primary_profile") != "causal_rolling_1s"
or len(worker_result.get("anchors", ())) != 20
):
raise M49TgsFailClosedError("M49 Worker result changed")
if (
input_manifest.get("schema_version") != "missioncore.m49-tgs-fail-closed-input/v1"
or input_manifest.get("coordinate_frame") != "map-gravity-local"
or len(input_manifest.get("records", ())) != 20
):
raise M49TgsFailClosedError("M49 input manifest changed")
if (
worker_summary.get("gpu_requested") is not False
and worker_summary.get("gpu_requested") is not None
):
raise M49TgsFailClosedError("M49 Worker GPU contract changed")
if (
worker_summary.get("aos_used") is not False
or worker_summary.get("all_eligible_points_accounted") is not True
or worker_summary.get("canonical_triton_health") != "healthy"
):
raise M49TgsFailClosedError("M49 Worker cleanup changed")
evidence = worker_result.get("evidence", {})
if (
evidence.get("path") != "evidence.npz"
or evidence.get("bytes") != (source / "evidence.npz").stat().st_size
or evidence.get("sha256") != _file_sha256(source / "evidence.npz")
or worker_result.get("input_manifest_sha256")
!= _file_sha256(source / "input-manifest.json")
):
raise M49TgsFailClosedError("M49 evidence proof changed")
def _artifact(
path: Path,
role: str,
schema_version: str | None,
media_type: str,
) -> dict[str, object]:
result: dict[str, object] = {
"role": role,
"path": path.name,
"byte_length": path.stat().st_size,
"sha256": _file_sha256(path),
"media_type": media_type,
}
if schema_version is not None:
result["schema_version"] = schema_version
return result
def _timing_metrics(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise M49TgsFailClosedError("M49 timing evidence is unavailable")
profiles: dict[str, list[float]] = {name: [] for name in M49_TGS_PROFILES}
maximum_rss = 0
lines = path.read_text(encoding="utf-8-sig").splitlines()
if not lines or lines[0] != "profile\tslot\twall_seconds\tmax_rss_kib":
raise M49TgsFailClosedError("M49 timing evidence changed")
for line in lines[1:]:
fields = line.split("\t")
if len(fields) != 4 or fields[0] not in profiles:
raise M49TgsFailClosedError("M49 timing row changed")
profiles[fields[0]].append(float(fields[2]))
maximum_rss = max(maximum_rss, int(fields[3]))
if any(len(values) != len(M49_TGS_ANCHORS) for values in profiles.values()):
raise M49TgsFailClosedError("M49 timing coverage changed")
result: dict[str, Any] = {"max_rss_kib": maximum_rss}
for name, values in profiles.items():
ordered = sorted(values)
result[name] = {
"p50_ms": ordered[len(ordered) // 2] * 1000,
"max_ms": max(ordered) * 1000,
}
return result
def _real_directory(path: Path, label: str) -> Path:
candidate = path.expanduser().absolute()
if candidate.is_symlink():
raise M49TgsFailClosedError(f"{label} must not be a symlink")
try:
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise M49TgsFailClosedError(f"{label} is unavailable") from exc
if not resolved.is_dir():
raise M49TgsFailClosedError(f"{label} is unavailable")
return resolved
def _json_file(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 M49TgsFailClosedError(f"{label} is unavailable")
try:
value = json.loads(path.read_text(encoding="utf-8-sig"))
except (json.JSONDecodeError, OSError) as exc:
raise M49TgsFailClosedError(f"{label} is invalid") from exc
if not isinstance(value, dict):
raise M49TgsFailClosedError(f"{label} is invalid")
return value
def _write_json(path: Path, value: object) -> None:
path.write_bytes(_canonical_json(value) + b"\n")
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def _canonical_sha256(value: object) -> str:
return hashlib.sha256(_canonical_json(value)).hexdigest()
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
digest.update(chunk)
return digest.hexdigest()
__all__ = [
"M49_TGS_ANCHORS",
"M49_TGS_PREFIX",
"M49_TGS_REPORT_SCHEMA",
"M49_TGS_RESULT_SCHEMA",
"M49TgsFailClosedError",
"M49TgsFailClosedResult",
"read_m49_tgs_fail_closed",
"seal_m49_tgs_fail_closed",
]
+12
View File
@@ -133,6 +133,7 @@ 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_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
from k1link.web.map_api import (
MapGatewayConfiguration,
MapGatewayProxy,
@@ -992,6 +993,17 @@ app.include_router(
),
)
)
app.include_router(
build_m49_tgs_fail_closed_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "m49"
/ "tgs-fail-closed-results"
),
)
)
app.include_router(
build_m48s_fixed_class_detector_lab_router(
root_provider=lambda: (
+284
View File
@@ -0,0 +1,284 @@
"""Read-only API for sealed gravity-aligned M49 TGS evidence."""
from __future__ import annotations
import copy
import json
import math
import re
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from typing import Final
import numpy as np
from fastapi import APIRouter, HTTPException, Query, Response
from k1link.laboratory.m49_tgs_fail_closed import (
M49_TGS_ANCHORS,
M49_TGS_PREFIX,
M49TgsFailClosedError,
M49TgsFailClosedResult,
read_m49_tgs_fail_closed,
)
RootProvider = Callable[[], Path | None]
RESULT_ID: Final = re.compile(rf"^{re.escape(M49_TGS_PREFIX)}[a-f0-9]{{64}}$")
RESULT_VIEW_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-view/v1"
RESULT_CATALOG_SCHEMA: Final = "missioncore.m49-tgs-fail-closed-catalog/v1"
ANCHOR_CATALOG_SCHEMA: Final = "missioncore.m49-tgs-anchor-catalog/v1"
ANCHOR_SPATIAL_SCHEMA: Final = "missioncore.m49-tgs-anchor-spatial/v1"
ENDPOINT_ROOT: Final = "/api/v1/laboratory/m49/tgs-fail-closed"
PROFILES: Final = ("current_increment", "causal_rolling_1s")
def build_m49_tgs_fail_closed_router(*, root_provider: RootProvider = lambda: None) -> APIRouter:
router = APIRouter(prefix=ENDPOINT_ROOT, tags=["laboratory"])
def result(result_id: str) -> M49TgsFailClosedResult:
candidate = _resolve_candidate(root_provider, result_id)
try:
return _read_result_cached(str(candidate), _signature(candidate))
except (M49TgsFailClosedError, OSError, ValueError):
raise HTTPException(status_code=404, detail="M49 TGS result not found") from None
@router.get("/results")
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
root = _configured_root(root_provider)
if root is None:
return _empty_catalog(configured=False)
items: list[dict[str, object]] = []
invalid_total = 0
for candidate in sorted(root.iterdir()):
if not candidate.is_dir() or RESULT_ID.fullmatch(candidate.name) is None:
continue
try:
sealed = _read_result_cached(str(candidate.resolve()), _signature(candidate))
items.append(_project_result(sealed))
except (M49TgsFailClosedError, OSError, ValueError):
invalid_total += 1
items.sort(
key=lambda item: (str(item["created_at_utc"]), str(item["result_id"])),
reverse=True,
)
return {
"schema_version": RESULT_CATALOG_SCHEMA,
"configured": True,
"items": items[:limit],
"candidate_total": len(items) + invalid_total,
"invalid_total": invalid_total,
"access": "read-only",
}
@router.get("/{result_id}")
def get_result(result_id: str) -> dict[str, object]:
return _project_result(result(result_id))
@router.get("/{result_id}/anchors")
def get_anchors(result_id: str) -> dict[str, object]:
sealed = result(result_id)
return {
"schema_version": ANCHOR_CATALOG_SCHEMA,
"result_id": result_id,
"linked_visual_result_id": sealed.report["visual_review"]["linked_visual_result_id"],
"anchors": copy.deepcopy(sealed.report["metrics"]["primary"]),
"anchor_count": len(M49_TGS_ANCHORS),
"profiles": list(PROFILES),
"default_profile": "causal_rolling_1s",
"access": "read-only",
}
@router.get("/{result_id}/anchors/{anchor_frame_index}/spatial")
def get_anchor_spatial(
result_id: str,
anchor_frame_index: int,
profile: str = Query(default="causal_rolling_1s"),
) -> Response:
if profile not in PROFILES or anchor_frame_index not in M49_TGS_ANCHORS:
raise HTTPException(status_code=404, detail="M49 TGS anchor not found")
sealed = result(result_id)
try:
content = _anchor_json_cached(
str(sealed.evidence_path),
result_id,
anchor_frame_index,
profile,
_evidence_signature(sealed.evidence_path),
)
except (KeyError, OSError, ValueError):
raise HTTPException(
status_code=503, detail="M49 TGS spatial evidence failed verification"
) from None
return Response(
content=content,
media_type="application/json",
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"X-Content-Type-Options": "nosniff",
},
)
return router
@lru_cache(maxsize=4)
def _read_result_cached(result_root: str, signature: tuple[int, ...]) -> M49TgsFailClosedResult:
del signature
return read_m49_tgs_fail_closed(Path(result_root))
@lru_cache(maxsize=24)
def _anchor_json_cached(
evidence_path: str,
result_id: str,
anchor_frame_index: int,
profile: str,
signature: tuple[int, int],
) -> bytes:
del signature
slot = M49_TGS_ANCHORS.index(anchor_frame_index)
with np.load(evidence_path, allow_pickle=False) as evidence:
offsets = evidence[f"{profile}_point_offsets"]
start = int(offsets[slot])
end = int(offsets[slot + 1])
points = evidence[f"{profile}_points_xyz_m"][start:end]
point_states = evidence[f"{profile}_point_states"][start:end]
centers = evidence["costmap_cell_centers_xy_m"]
cell_states = evidence[f"{profile}_costmap_states"][slot]
z_bounds = evidence[f"{profile}_costmap_z_bounds_m"][slot]
if (
points.shape[1:] != (3,)
or point_states.shape != (points.shape[0],)
or centers.shape != (2244, 2)
or cell_states.shape != (2244,)
or z_bounds.shape != (2244, 2)
or not np.isfinite(points).all()
or not np.isfinite(centers).all()
or not np.isin(point_states, np.asarray([1, 2, 3], dtype=np.uint8)).all()
or not np.isin(cell_states, np.asarray([0, 1, 2, 3], dtype=np.uint8)).all()
):
raise ValueError("M49 TGS spatial shape changed")
payload = {
"schema_version": ANCHOR_SPATIAL_SCHEMA,
"result_id": result_id,
"anchor_frame_index": anchor_frame_index,
"source_sequence": anchor_frame_index,
"profile": profile,
"coordinate_frame": "map-gravity-local",
"points_xyz_m": points.astype(float).tolist(),
"point_states": point_states.astype(int).tolist(),
"costmap": {
"cell_size_m": 0.45,
"radius_m": 12.0,
"centers_xy_m": centers.astype(float).tolist(),
"states": cell_states.astype(int).tolist(),
"z_bounds_m": [
[
float(row[0]) if math.isfinite(float(row[0])) else None,
float(row[1]) if math.isfinite(float(row[1])) else None,
]
for row in z_bounds
],
},
"state_codes": {
"UNOBSERVED": 0,
"GROUND_SUPPORT": 1,
"NONGROUND_OCCUPIED": 2,
"UNKNOWN_REJECTED": 3,
},
"all_points_accounted": True,
"aos_used": False,
"authority": {
"visual_quality_accepted": False,
"navigation_or_safety_accepted": False,
},
"access": "read-only",
}
return json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def _project_result(result: M49TgsFailClosedResult) -> dict[str, object]:
report = result.report
return {
"schema_version": RESULT_VIEW_SCHEMA,
"result_id": result.result_id,
"created_at_utc": result.manifest["created_at_utc"],
"source": copy.deepcopy(report["source"]),
"configuration": copy.deepcopy(report["configuration"]),
"method": copy.deepcopy(report["method"]),
"execution": copy.deepcopy(report["execution"]),
"metrics": copy.deepcopy(report["metrics"]),
"acceptance": copy.deepcopy(report["acceptance"]),
"decision": copy.deepcopy(report["decision"]),
"limitations": copy.deepcopy(report["limitations"]),
"authority": copy.deepcopy(report["authority"]),
"visual_review": copy.deepcopy(report["visual_review"]),
"ground_truth": False,
"access": "read-only",
}
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
if RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="M49 TGS result not found")
root = _configured_root(provider)
if root is None:
raise HTTPException(status_code=404, detail="M49 TGS result not found")
candidate = root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(status_code=404, detail="M49 TGS result not found")
resolved = candidate.resolve(strict=True)
if resolved.parent != root:
raise HTTPException(status_code=404, detail="M49 TGS result not found")
return resolved
def _configured_root(provider: RootProvider) -> Path | None:
value = provider()
if value is None or value.is_symlink() or not value.is_dir():
return None
return value.resolve(strict=True)
def _signature(candidate: Path) -> tuple[int, ...]:
result: list[int] = []
for name in (
"manifest.json",
"report.json",
"evidence.npz",
"worker-summary.json",
"input-manifest.json",
"tgs-timing.tsv",
):
path = candidate / name
if path.is_symlink() or not path.is_file():
raise ValueError("M49 TGS artifact unavailable")
stat = path.stat()
result.extend((stat.st_size, stat.st_mtime_ns))
return tuple(result)
def _evidence_signature(path: Path) -> tuple[int, int]:
stat = path.stat()
return stat.st_size, stat.st_mtime_ns
def _empty_catalog(*, configured: bool) -> dict[str, object]:
return {
"schema_version": RESULT_CATALOG_SCHEMA,
"configured": configured,
"items": [],
"candidate_total": 0,
"invalid_total": 0,
"access": "read-only",
}
__all__ = ["build_m49_tgs_fail_closed_router"]
+2 -1
View File
@@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
repository_root / "config" / "laboratories"
)
assert len(registry.definitions) == 39
assert len(registry.definitions) == 41
assert {item.work_id for item in registry.definitions} >= {
"e31-source-binding",
"e46j-raw-fisheye-realtime",
@@ -144,6 +144,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
"m48-static-occupancy-qualification",
"m48s-fixed-class-detector",
"m48t-risk-quality-temporal",
"m49-tgs-fail-closed-evidence",
}
m48 = next(
item for item in registry.definitions if item.work_id == "m48-object-centric-quality"
+4
View File
@@ -101,6 +101,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
"e47-semantic-slam-shadow",
"m48s-fixed-class-detector",
"m48t-risk-quality-temporal",
"m49-tgs-fail-closed-evidence",
}
by_work_id = {row.work_id: row for row in execution.definitions}
assert by_work_id["m48-small-static-passage-regression"].evidence_contract == (
@@ -118,6 +119,8 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
assert by_work_id["m48s-fixed-class-detector"].isolation == "bounded-adapter"
assert by_work_id["m48t-risk-quality-temporal"].lifecycle == "experimental"
assert by_work_id["m48t-risk-quality-temporal"].isolation == "bounded-adapter"
assert by_work_id["m49-tgs-fail-closed-evidence"].lifecycle == "experimental"
assert by_work_id["m49-tgs-fail-closed-evidence"].isolation == "bounded-adapter"
assert all(
row.lifecycle == "canonical"
for row in execution.definitions
@@ -126,6 +129,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
"e47-semantic-slam-shadow",
"m48s-fixed-class-detector",
"m48t-risk-quality-temporal",
"m49-tgs-fail-closed-evidence",
}
)
assert len(execution.definitions) + len(execution.legacy_work_ids) == len(
@@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
root / "config" / "laboratory-value-review.json"
)
assert len(registry.entries) == 38
assert len(registry.entries) == 39
assert {entry.catalog_id for entry in registry.entries} >= {
"e28-local-surface",
"e46d-temporal-failure-audit",
@@ -91,4 +91,5 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
"m48-static-occupancy-qualification",
"m48s-fixed-class-detector",
"m48t-risk-quality-temporal",
"m49-tgs-fail-closed-evidence",
}
+165
View File
@@ -0,0 +1,165 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import numpy as np
from fastapi import FastAPI
from fastapi.testclient import TestClient
from k1link.laboratory.m49_tgs_fail_closed import (
M49_TGS_ANCHORS,
read_m49_tgs_fail_closed,
seal_m49_tgs_fail_closed,
)
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
def _sha(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _write_json(path: Path, value: object) -> None:
path.write_text(json.dumps(value, sort_keys=True), encoding="utf-8")
def _source(root: Path) -> Path:
root.mkdir()
centers = np.zeros((2244, 2), dtype=np.float32)
arrays: dict[str, np.ndarray] = {"costmap_cell_centers_xy_m": centers}
summaries: list[dict[str, object]] = []
for profile in ("current_increment", "causal_rolling_1s"):
arrays[f"{profile}_points_xyz_m"] = np.asarray(
[[float(index), 0.0, 0.1] for index in range(10)], dtype=np.float32
)
arrays[f"{profile}_point_states"] = np.asarray([1, 2] * 5, dtype=np.uint8)
arrays[f"{profile}_point_offsets"] = np.arange(11, dtype=np.int64)
arrays[f"{profile}_costmap_states"] = np.zeros((10, 2244), dtype=np.uint8)
arrays[f"{profile}_costmap_ground_point_counts"] = np.zeros((10, 2244), dtype=np.int32)
arrays[f"{profile}_costmap_nonground_point_counts"] = np.zeros((10, 2244), dtype=np.int32)
arrays[f"{profile}_costmap_rejected_point_counts"] = np.zeros((10, 2244), dtype=np.int32)
arrays[f"{profile}_costmap_z_bounds_m"] = np.full((10, 2244, 2), np.nan, dtype=np.float32)
for slot, anchor in enumerate(M49_TGS_ANCHORS):
summaries.append(
{
"profile_id": profile,
"slot": slot,
"anchor_frame_index": anchor,
"point_count": 1,
"ground_point_count": 1 if slot % 2 == 0 else 0,
"nonground_point_count": 0 if slot % 2 == 0 else 1,
"rejected_point_count": 0,
"ground_cell_count": 0,
"nonground_cell_count": 0,
"rejected_cell_count": 0,
"unobserved_cell_count": 2244,
"all_points_accounted": True,
}
)
np.savez_compressed(root / "evidence.npz", **arrays)
records = [
{
"profile_id": profile,
"slot": slot,
"anchor_frame_index": anchor,
}
for profile in ("current_increment", "causal_rolling_1s")
for slot, anchor in enumerate(M49_TGS_ANCHORS)
]
_write_json(
root / "input-manifest.json",
{
"schema_version": "missioncore.m49-tgs-fail-closed-input/v1",
"coordinate_frame": "map-gravity-local",
"records": records,
},
)
_write_json(
root / "worker-summary.json",
{
"schema_version": "missioncore.m49-tgs-worker-summary/v1",
"code_revision": "a" * 40,
"wall_seconds": 1.5,
"free_memory_gib_before": 42.0,
"canonical_triton_id": "b" * 64,
"canonical_triton_health": "healthy",
"all_eligible_points_accounted": True,
"aos_used": False,
},
)
timing = ["profile\tslot\twall_seconds\tmax_rss_kib"]
timing.extend(
f"{profile}\t{slot}\t0.02\t7000"
for profile in ("current_increment", "causal_rolling_1s")
for slot in range(10)
)
(root / "tgs-timing.tsv").write_text("\n".join(timing) + "\n", encoding="utf-8")
_write_json(
root / "result.json",
{
"schema_version": "missioncore.m49-tgs-fail-closed-evidence-result/v1",
"status": "passed",
"config_sha256": "c" * 64,
"source_pack_sha256": "d" * 64,
"input_manifest_sha256": _sha(root / "input-manifest.json"),
"evidence": {
"path": "evidence.npz",
"bytes": (root / "evidence.npz").stat().st_size,
"sha256": _sha(root / "evidence.npz"),
},
"costmap": {
"coordinate_frame": "map-gravity-local",
"cell_size_m": 0.45,
"radius_m": 12.0,
"cell_count": 2244,
},
"summary": {
"aos_used": False,
"all_eligible_points_accounted": True,
"primary_profile": "causal_rolling_1s",
},
"anchors": summaries,
},
)
return root
def test_m49_tgs_seal_and_api_are_immutable_and_fail_closed(tmp_path: Path) -> None:
source = _source(tmp_path / "source")
destination = tmp_path / "results"
linked = "m4-threat-replay-" + "e" * 64
sealed = seal_m49_tgs_fail_closed(
source_root=source,
destination_root=destination,
profile_path=(
REPOSITORY_ROOT / "config" / "perception" / "m49-tgs-fail-closed-evidence-v1.json"
),
linked_visual_result_id=linked,
created_at_utc="2026-08-26T17:30:00Z",
)
assert read_m49_tgs_fail_closed(sealed.root).result_id == sealed.result_id
app = FastAPI()
app.include_router(build_m49_tgs_fail_closed_router(root_provider=lambda: destination))
client = TestClient(app)
catalog = client.get("/api/v1/laboratory/m49/tgs-fail-closed/results")
assert catalog.status_code == 200
assert catalog.json()["items"][0]["result_id"] == sealed.result_id
anchors = client.get(f"/api/v1/laboratory/m49/tgs-fail-closed/{sealed.result_id}/anchors")
assert anchors.status_code == 200
assert anchors.json()["anchor_count"] == 10
spatial = client.get(
f"/api/v1/laboratory/m49/tgs-fail-closed/{sealed.result_id}/anchors/171/spatial"
)
assert spatial.status_code == 200
assert spatial.json()["point_states"] == [1]
assert spatial.json()["costmap"]["states"] == [0] * 2244
assert spatial.json()["authority"]["navigation_or_safety_accepted"] is False
(sealed.root / "worker-summary.json").write_text("{}", encoding="utf-8")
assert (
client.get(f"/api/v1/laboratory/m49/tgs-fail-closed/{sealed.result_id}").status_code == 404
)