feat(lab): publish fail-closed TGS evidence
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 `1–80 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",
|
||||
]
|
||||
@@ -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: (
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
Reference in New Issue
Block a user