feat(control-station): present static occupancy qualification
This commit is contained in:
@@ -42,12 +42,14 @@ import {
|
|||||||
fetchM48LifecycleResult,
|
fetchM48LifecycleResult,
|
||||||
} from "./m48ObjectCentricQuality";
|
} from "./m48ObjectCentricQuality";
|
||||||
import { fetchM48SmallStaticRegression } from "./m48SmallStaticRegression";
|
import { fetchM48SmallStaticRegression } from "./m48SmallStaticRegression";
|
||||||
|
import { fetchM48StaticOccupancyQualification } from "./m48StaticOccupancyQualification";
|
||||||
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||||
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
||||||
|
|
||||||
export type AdvancedLaboratoryWorkId =
|
export type AdvancedLaboratoryWorkId =
|
||||||
| "m48-object-centric-quality"
|
| "m48-object-centric-quality"
|
||||||
| "m48-small-static-passage-regression"
|
| "m48-small-static-passage-regression"
|
||||||
|
| "m48-static-occupancy-qualification"
|
||||||
| "m48s-fixed-class-detector"
|
| "m48s-fixed-class-detector"
|
||||||
| "m48t-risk-quality-temporal"
|
| "m48t-risk-quality-temporal"
|
||||||
| "m47-reference-graph-shadow"
|
| "m47-reference-graph-shadow"
|
||||||
@@ -94,6 +96,7 @@ export interface AdvancedLaboratoryIndexItem {
|
|||||||
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||||
"m48-object-centric-quality",
|
"m48-object-centric-quality",
|
||||||
"m48-small-static-passage-regression",
|
"m48-small-static-passage-regression",
|
||||||
|
"m48-static-occupancy-qualification",
|
||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
"m47-reference-graph-shadow",
|
"m47-reference-graph-shadow",
|
||||||
@@ -135,6 +138,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
|||||||
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
||||||
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
|
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
|
||||||
"m48-small-static-passage-regression": "m48-small-static-passage-regression",
|
"m48-small-static-passage-regression": "m48-small-static-passage-regression",
|
||||||
|
"m48-static-occupancy-qualification": "m48-static-occupancy-qualification",
|
||||||
"m48s-fixed-class-detector": "m48s-fixed-class-detector-lab",
|
"m48s-fixed-class-detector": "m48s-fixed-class-detector-lab",
|
||||||
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
|
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
|
||||||
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
||||||
@@ -184,6 +188,7 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
|||||||
m47Graph: null,
|
m47Graph: null,
|
||||||
m48: null,
|
m48: null,
|
||||||
m48SmallStatic: null,
|
m48SmallStatic: null,
|
||||||
|
m48StaticOccupancy: null,
|
||||||
m48s: null,
|
m48s: null,
|
||||||
m48t: null,
|
m48t: null,
|
||||||
m4Threat: null,
|
m4Threat: null,
|
||||||
@@ -312,6 +317,7 @@ export function advancedLaboratoryResultAvailable(
|
|||||||
): boolean {
|
): boolean {
|
||||||
return workId === "m48-object-centric-quality" ? results.m48 !== null
|
return workId === "m48-object-centric-quality" ? results.m48 !== null
|
||||||
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
|
: workId === "m48-small-static-passage-regression" ? results.m48SmallStatic !== null
|
||||||
|
: workId === "m48-static-occupancy-qualification" ? results.m48StaticOccupancy !== null
|
||||||
: workId === "m48s-fixed-class-detector" ? results.m48s !== null
|
: workId === "m48s-fixed-class-detector" ? results.m48s !== null
|
||||||
: workId === "m48t-risk-quality-temporal" ? results.m48t !== null
|
: workId === "m48t-risk-quality-temporal" ? results.m48t !== null
|
||||||
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
|
: workId === "m47-reference-graph-shadow" ? results.m47Graph !== null
|
||||||
@@ -369,6 +375,9 @@ export async function fetchAdvancedLaboratoryResult(
|
|||||||
} else if (workId === "m48-small-static-passage-regression") {
|
} else if (workId === "m48-small-static-passage-regression") {
|
||||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R1 regression identity не выбрана.");
|
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R1 regression identity не выбрана.");
|
||||||
results.m48SmallStatic = await fetchM48SmallStaticRegression(resultId, { fetcher, signal });
|
results.m48SmallStatic = await fetchM48SmallStaticRegression(resultId, { fetcher, signal });
|
||||||
|
} else if (workId === "m48-static-occupancy-qualification") {
|
||||||
|
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R2 qualification identity не выбрана.");
|
||||||
|
results.m48StaticOccupancy = await fetchM48StaticOccupancyQualification(resultId, { fetcher, signal });
|
||||||
} else if (workId === "m48s-fixed-class-detector") {
|
} else if (workId === "m48s-fixed-class-detector") {
|
||||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8S LAB identity не выбрана.");
|
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8S LAB identity не выбрана.");
|
||||||
results.m48s = await fetchM48SFixedClassDetectorResult(resultId, { fetcher, signal });
|
results.m48s = await fetchM48SFixedClassDetectorResult(resultId, { fetcher, signal });
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import type { M4ThreatReplayResult } from "./m4ReplayThreat";
|
|||||||
import type { M47ReferenceGraphLabResult } from "./m47ReferenceGraph";
|
import type { M47ReferenceGraphLabResult } from "./m47ReferenceGraph";
|
||||||
import type { M48AdvancedResult } from "./m48ObjectCentricQuality";
|
import type { M48AdvancedResult } from "./m48ObjectCentricQuality";
|
||||||
import type { M48SmallStaticRegressionResult } from "./m48SmallStaticRegression";
|
import type { M48SmallStaticRegressionResult } from "./m48SmallStaticRegression";
|
||||||
|
import type { M48StaticOccupancyQualificationResult } from "./m48StaticOccupancyQualification";
|
||||||
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||||
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
|
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ export interface AdvancedLaboratoryResults {
|
|||||||
m47Graph: M47ReferenceGraphLabResult | null;
|
m47Graph: M47ReferenceGraphLabResult | null;
|
||||||
m48: M48AdvancedResult | null;
|
m48: M48AdvancedResult | null;
|
||||||
m48SmallStatic: M48SmallStaticRegressionResult | null;
|
m48SmallStatic: M48SmallStaticRegressionResult | null;
|
||||||
|
m48StaticOccupancy: M48StaticOccupancyQualificationResult | null;
|
||||||
m48s: M48SFixedClassDetectorResult | null;
|
m48s: M48SFixedClassDetectorResult | null;
|
||||||
m48t: M48TRiskQualityResult | null;
|
m48t: M48TRiskQualityResult | null;
|
||||||
m4Threat: M4ThreatReplayResult | null;
|
m4Threat: M4ThreatReplayResult | null;
|
||||||
|
|||||||
@@ -967,10 +967,9 @@ export async function fetchAdvancedLaboratoryResults({
|
|||||||
const e39 = settledCatalogValue(settled[7]);
|
const e39 = settledCatalogValue(settled[7]);
|
||||||
const e40 = settledCatalogValue(settled[8]);
|
const e40 = settledCatalogValue(settled[8]);
|
||||||
return {
|
return {
|
||||||
m47Graph: null, m48: null, m48SmallStatic: null, m48s: null, m48t: null, m4Threat: null,
|
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
||||||
l3: null, l31: null,
|
m48s: null, m48t: null, m4Threat: null,
|
||||||
l32: null,
|
l3: null, l31: null, l32: null, l33: null,
|
||||||
l33: null,
|
|
||||||
e31,
|
e31,
|
||||||
e32,
|
e32,
|
||||||
e33,
|
e33,
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import type { LaboratoryFetch } from "./advancedResults";
|
||||||
|
import type { M48Authority } from "./m48ObjectCentricQuality";
|
||||||
|
|
||||||
|
const RESULT_ID = /^m48-static-occupancy-qualification-[a-f0-9]{64}$/;
|
||||||
|
const M47_RESULT_ID = /^m47-reference-graph-lab-[a-f0-9]{64}$/;
|
||||||
|
const M48R1_RESULT_ID = /^m48-small-static-passage-regression-[a-f0-9]{64}$/;
|
||||||
|
const ANCHOR_ID = /^anchor-[a-f0-9]{24}$/;
|
||||||
|
|
||||||
|
export interface M48StaticOccupancyMetrics {
|
||||||
|
operatorStaticAnchorCount: number;
|
||||||
|
baselineQualifiedCount: number;
|
||||||
|
candidateQualifiedCount: number;
|
||||||
|
unresolvedUnknownCount: number;
|
||||||
|
criticalNearAnchorCount: number;
|
||||||
|
criticalNearBaselineRecall: number;
|
||||||
|
criticalNearCandidateRecall: number;
|
||||||
|
approachAnchorCount: number;
|
||||||
|
approachBaselineRecall: number;
|
||||||
|
approachCandidateRecall: number;
|
||||||
|
canonicalEngineeringAnchorCount: number;
|
||||||
|
canonicalEngineeringRecall: number;
|
||||||
|
falseFreeCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M48StaticOccupancyQualificationResult {
|
||||||
|
resultId: string;
|
||||||
|
referenceGraphLabResultId: string;
|
||||||
|
smallStaticResultId: string;
|
||||||
|
createdAtUtc: string;
|
||||||
|
runLabel: "M4.8R2";
|
||||||
|
pipelineId: "m4-current-rolling-plus-step-static-occupancy/v1";
|
||||||
|
experimentId: "m48-static-occupancy-qualification/v1";
|
||||||
|
accepted: boolean;
|
||||||
|
metrics: M48StaticOccupancyMetrics;
|
||||||
|
gates: {
|
||||||
|
criticalNearCandidateRecall: boolean;
|
||||||
|
approachCandidateRecall: boolean;
|
||||||
|
canonicalEngineeringRecall: boolean;
|
||||||
|
zeroFalseFree: boolean;
|
||||||
|
independentTruthAvailable: false;
|
||||||
|
};
|
||||||
|
decision: {
|
||||||
|
state: "accepted-bounded-static-occupancy-qualification" | "partial-static-occupancy-qualification";
|
||||||
|
criticalNearCandidateReadyForShadow: boolean;
|
||||||
|
productionAccepted: false;
|
||||||
|
summary: string;
|
||||||
|
nextAction: string;
|
||||||
|
};
|
||||||
|
authority: M48Authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M48StaticOccupancyCase {
|
||||||
|
anchorId: string;
|
||||||
|
clipId: string;
|
||||||
|
sequence: number;
|
||||||
|
extentXyxy: readonly [number, number, number, number];
|
||||||
|
distanceM: number;
|
||||||
|
distanceBand: "critical-near" | "approach" | "outside-qualified-bands";
|
||||||
|
baselineQualified: boolean;
|
||||||
|
candidateQualified: boolean;
|
||||||
|
outcome: "candidate-qualified" | "unresolved-unknown-never-free";
|
||||||
|
graphMatched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class M48StaticOccupancyContractError extends Error {}
|
||||||
|
|
||||||
|
function objectValue(value: unknown, label: string): Record<string, unknown> {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
throw new M48StaticOccupancyContractError(`${label}: ожидался объект.`);
|
||||||
|
}
|
||||||
|
return value as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(value: unknown, label: string): string {
|
||||||
|
if (typeof value !== "string" || !value.trim()) {
|
||||||
|
throw new M48StaticOccupancyContractError(`${label}: ожидалась строка.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberValue(value: unknown, label: string): number {
|
||||||
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||||
|
throw new M48StaticOccupancyContractError(`${label}: ожидалось число.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function integer(value: unknown, label: string): number {
|
||||||
|
const parsed = numberValue(value, label);
|
||||||
|
if (!Number.isInteger(parsed) || parsed < 0) {
|
||||||
|
throw new M48StaticOccupancyContractError(`${label}: ожидалось целое число.`);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bool(value: unknown, label: string): boolean {
|
||||||
|
if (typeof value !== "boolean") {
|
||||||
|
throw new M48StaticOccupancyContractError(`${label}: ожидался флаг.`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exact(value: unknown, expected: string | boolean, label: string): void {
|
||||||
|
if (value !== expected) throw new M48StaticOccupancyContractError(`${label}: нарушен контракт.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function member<T extends string>(value: unknown, allowed: readonly T[], label: string): T {
|
||||||
|
const parsed = text(value, label);
|
||||||
|
if (!allowed.includes(parsed as T)) {
|
||||||
|
throw new M48StaticOccupancyContractError(`${label}: недопустимое значение.`);
|
||||||
|
}
|
||||||
|
return parsed as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extent(value: unknown): readonly [number, number, number, number] {
|
||||||
|
if (!Array.isArray(value) || value.length !== 4 || value.some((item) => typeof item !== "number" || !Number.isFinite(item))) {
|
||||||
|
throw new M48StaticOccupancyContractError("M4.8R2.case.extent: нарушен контракт.");
|
||||||
|
}
|
||||||
|
const result = value as number[];
|
||||||
|
if (result.some((item) => item < 0 || item > 1) || result[0]! >= result[2]! || result[1]! >= result[3]!) {
|
||||||
|
throw new M48StaticOccupancyContractError("M4.8R2.case.extent: нарушена геометрия.");
|
||||||
|
}
|
||||||
|
return result as unknown as readonly [number, number, number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
function authority(value: unknown): M48Authority {
|
||||||
|
const row = objectValue(value, "M4.8R2.authority");
|
||||||
|
exact(row.mode, "replay-simulated", "M4.8R2.authority.mode");
|
||||||
|
exact(row.physical_live, false, "M4.8R2.authority.physical_live");
|
||||||
|
exact(row.commands_enabled, false, "M4.8R2.authority.commands_enabled");
|
||||||
|
exact(row.actuation_allowed, false, "M4.8R2.authority.actuation_allowed");
|
||||||
|
exact(row.navigation_or_safety_accepted, false, "M4.8R2.authority.navigation_or_safety_accepted");
|
||||||
|
return { mode: "replay-simulated", physicalLive: false, commandsEnabled: false, actuationAllowed: false, navigationOrSafetyAccepted: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function metrics(value: unknown): M48StaticOccupancyMetrics {
|
||||||
|
const row = objectValue(value, "M4.8R2.metrics");
|
||||||
|
return {
|
||||||
|
operatorStaticAnchorCount: integer(row.operator_static_anchor_count, "operator_static_anchor_count"),
|
||||||
|
baselineQualifiedCount: integer(row.baseline_qualified_count, "baseline_qualified_count"),
|
||||||
|
candidateQualifiedCount: integer(row.candidate_qualified_count, "candidate_qualified_count"),
|
||||||
|
unresolvedUnknownCount: integer(row.unresolved_unknown_count, "unresolved_unknown_count"),
|
||||||
|
criticalNearAnchorCount: integer(row.critical_near_anchor_count, "critical_near_anchor_count"),
|
||||||
|
criticalNearBaselineRecall: numberValue(row.critical_near_baseline_recall, "critical_near_baseline_recall"),
|
||||||
|
criticalNearCandidateRecall: numberValue(row.critical_near_candidate_recall, "critical_near_candidate_recall"),
|
||||||
|
approachAnchorCount: integer(row.approach_anchor_count, "approach_anchor_count"),
|
||||||
|
approachBaselineRecall: numberValue(row.approach_baseline_recall, "approach_baseline_recall"),
|
||||||
|
approachCandidateRecall: numberValue(row.approach_candidate_recall, "approach_candidate_recall"),
|
||||||
|
canonicalEngineeringAnchorCount: integer(row.canonical_engineering_anchor_count, "canonical_engineering_anchor_count"),
|
||||||
|
canonicalEngineeringRecall: numberValue(row.canonical_engineering_recall, "canonical_engineering_recall"),
|
||||||
|
falseFreeCount: integer(row.false_free_count, "false_free_count"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchM48StaticOccupancyQualification(
|
||||||
|
resultId: string,
|
||||||
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
|
): Promise<M48StaticOccupancyQualificationResult> {
|
||||||
|
if (!RESULT_ID.test(resultId)) throw new M48StaticOccupancyContractError("M4.8R2 identity недопустима.");
|
||||||
|
const response = await fetcher(`/api/v1/laboratory/m48/regressions/static-occupancy/${encodeURIComponent(resultId)}`, { method: "GET", headers: { Accept: "application/json" }, signal });
|
||||||
|
if (!response.ok) throw new M48StaticOccupancyContractError(`M4.8R2 недоступен: HTTP ${response.status}.`);
|
||||||
|
const payload = objectValue(await response.json(), "M4.8R2");
|
||||||
|
exact(payload.schema_version, "missioncore.m48-static-occupancy-qualification-result-view/v1", "M4.8R2.schema_version");
|
||||||
|
const m47 = text(payload.reference_graph_lab_result_id, "M4.8R2.reference_graph_lab_result_id");
|
||||||
|
const m48r1 = text(payload.small_static_result_id, "M4.8R2.small_static_result_id");
|
||||||
|
if (!M47_RESULT_ID.test(m47) || !M48R1_RESULT_ID.test(m48r1)) throw new M48StaticOccupancyContractError("M4.8R2 source identity нарушена.");
|
||||||
|
exact(payload.run_label, "M4.8R2", "M4.8R2.run_label");
|
||||||
|
exact(payload.pipeline_id, "m4-current-rolling-plus-step-static-occupancy/v1", "M4.8R2.pipeline_id");
|
||||||
|
exact(payload.experiment_id, "m48-static-occupancy-qualification/v1", "M4.8R2.experiment_id");
|
||||||
|
exact(payload.ground_truth, false, "M4.8R2.ground_truth");
|
||||||
|
exact(payload.independent_truth, false, "M4.8R2.independent_truth");
|
||||||
|
const gates = objectValue(payload.gates, "M4.8R2.gates");
|
||||||
|
const decision = objectValue(payload.decision, "M4.8R2.decision");
|
||||||
|
exact(decision.production_accepted, false, "M4.8R2.decision.production_accepted");
|
||||||
|
return {
|
||||||
|
resultId,
|
||||||
|
referenceGraphLabResultId: m47,
|
||||||
|
smallStaticResultId: m48r1,
|
||||||
|
createdAtUtc: text(payload.created_at_utc, "M4.8R2.created_at_utc"),
|
||||||
|
runLabel: "M4.8R2",
|
||||||
|
pipelineId: "m4-current-rolling-plus-step-static-occupancy/v1",
|
||||||
|
experimentId: "m48-static-occupancy-qualification/v1",
|
||||||
|
accepted: bool(payload.accepted, "M4.8R2.accepted"),
|
||||||
|
metrics: metrics(payload.metrics),
|
||||||
|
gates: {
|
||||||
|
criticalNearCandidateRecall: bool(gates.critical_near_candidate_recall, "critical_near_candidate_recall"),
|
||||||
|
approachCandidateRecall: bool(gates.approach_candidate_recall, "approach_candidate_recall"),
|
||||||
|
canonicalEngineeringRecall: bool(gates.canonical_engineering_recall, "canonical_engineering_recall"),
|
||||||
|
zeroFalseFree: bool(gates.zero_false_free, "zero_false_free"),
|
||||||
|
independentTruthAvailable: false,
|
||||||
|
},
|
||||||
|
decision: {
|
||||||
|
state: member(
|
||||||
|
decision.state,
|
||||||
|
[
|
||||||
|
"accepted-bounded-static-occupancy-qualification",
|
||||||
|
"partial-static-occupancy-qualification",
|
||||||
|
] as const,
|
||||||
|
"M4.8R2.decision.state",
|
||||||
|
),
|
||||||
|
criticalNearCandidateReadyForShadow: bool(decision.critical_near_candidate_ready_for_shadow, "critical_near_candidate_ready_for_shadow"),
|
||||||
|
productionAccepted: false,
|
||||||
|
summary: text(decision.summary, "M4.8R2.decision.summary"),
|
||||||
|
nextAction: text(decision.next_action, "M4.8R2.decision.next_action"),
|
||||||
|
},
|
||||||
|
authority: authority(payload.authority),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchM48StaticOccupancyCases(
|
||||||
|
resultId: string,
|
||||||
|
{ fetcher = fetch, signal }: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
|
): Promise<readonly M48StaticOccupancyCase[]> {
|
||||||
|
if (!RESULT_ID.test(resultId)) throw new M48StaticOccupancyContractError("M4.8R2 identity недопустима.");
|
||||||
|
const response = await fetcher(`/api/v1/laboratory/m48/regressions/static-occupancy/${encodeURIComponent(resultId)}/cases`, { method: "GET", headers: { Accept: "application/json" }, signal });
|
||||||
|
if (!response.ok) throw new M48StaticOccupancyContractError(`M4.8R2 cases недоступны: HTTP ${response.status}.`);
|
||||||
|
const payload = objectValue(await response.json(), "M4.8R2 cases");
|
||||||
|
exact(payload.schema_version, "missioncore.m48-static-occupancy-case-catalog/v1", "M4.8R2 cases.schema_version");
|
||||||
|
if (!Array.isArray(payload.cases) || payload.cases.length !== integer(payload.case_count, "M4.8R2.case_count")) throw new M48StaticOccupancyContractError("M4.8R2 cases: размер изменился.");
|
||||||
|
return payload.cases.map((value, index) => {
|
||||||
|
const row = objectValue(value, `M4.8R2.case[${index}]`);
|
||||||
|
const anchorId = text(row.anchor_id, "anchor_id");
|
||||||
|
if (!ANCHOR_ID.test(anchorId)) throw new M48StaticOccupancyContractError("M4.8R2 anchor identity нарушена.");
|
||||||
|
const graph = objectValue(row.accepted_graph, "accepted_graph");
|
||||||
|
const band = member(
|
||||||
|
row.distance_band,
|
||||||
|
["critical-near", "approach", "outside-qualified-bands"] as const,
|
||||||
|
"distance_band",
|
||||||
|
);
|
||||||
|
const outcome = member(
|
||||||
|
row.outcome,
|
||||||
|
["candidate-qualified", "unresolved-unknown-never-free"] as const,
|
||||||
|
"outcome",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
anchorId,
|
||||||
|
clipId: text(row.clip_id, "clip_id"),
|
||||||
|
sequence: integer(row.sequence, "sequence"),
|
||||||
|
extentXyxy: extent(row.extent_xyxy),
|
||||||
|
distanceM: numberValue(row.distance_m, "distance_m"),
|
||||||
|
distanceBand: band,
|
||||||
|
baselineQualified: bool(row.baseline_qualified, "baseline_qualified"),
|
||||||
|
candidateQualified: bool(row.candidate_qualified, "candidate_qualified"),
|
||||||
|
outcome,
|
||||||
|
graphMatched: bool(graph.matched, "accepted_graph.matched"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ import { M4ReplayThreatResultView } from "./M4ReplayThreatResult";
|
|||||||
import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult";
|
import { M47ReferenceGraphResultView } from "./M47ReferenceGraphResult";
|
||||||
import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
|
import { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
|
||||||
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
|
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
|
||||||
|
import { M48StaticOccupancyQualificationResultView } from "./M48StaticOccupancyQualificationResult";
|
||||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||||
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
||||||
|
|
||||||
@@ -94,6 +95,9 @@ export function AdvancedLaboratoryResult({
|
|||||||
if (workId === "m48-small-static-passage-regression" && results.m48SmallStatic) {
|
if (workId === "m48-small-static-passage-regression" && results.m48SmallStatic) {
|
||||||
return <M48SmallStaticPassageRegressionResultView rigLabel={rigLabel} result={results.m48SmallStatic} />;
|
return <M48SmallStaticPassageRegressionResultView rigLabel={rigLabel} result={results.m48SmallStatic} />;
|
||||||
}
|
}
|
||||||
|
if (workId === "m48-static-occupancy-qualification" && results.m48StaticOccupancy) {
|
||||||
|
return <M48StaticOccupancyQualificationResultView rigLabel={rigLabel} result={results.m48StaticOccupancy} />;
|
||||||
|
}
|
||||||
if (workId === "m48s-fixed-class-detector" && results.m48s) {
|
if (workId === "m48s-fixed-class-detector" && results.m48s) {
|
||||||
return <M48SFixedClassDetectorResultView rigLabel={rigLabel} result={results.m48s} />;
|
return <M48SFixedClassDetectorResultView rigLabel={rigLabel} result={results.m48s} />;
|
||||||
}
|
}
|
||||||
|
|||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { Icon } from "@nodedc/ui-react";
|
||||||
|
|
||||||
|
import { fetchM47ReferenceGraphLab, type M47ReferenceGraphLabResult } from "../../core/laboratory/m47ReferenceGraph";
|
||||||
|
import {
|
||||||
|
fetchM48StaticOccupancyCases,
|
||||||
|
type M48StaticOccupancyCase,
|
||||||
|
type M48StaticOccupancyQualificationResult,
|
||||||
|
} from "../../core/laboratory/m48StaticOccupancyQualification";
|
||||||
|
import {
|
||||||
|
M4ReplayThreatVisual,
|
||||||
|
type M4ReplayThreatReviewAnchor,
|
||||||
|
} from "./M4ReplayThreatVisual";
|
||||||
|
|
||||||
|
function message(error: unknown): string {
|
||||||
|
return error instanceof Error && error.message.trim()
|
||||||
|
? error.message
|
||||||
|
: "Каноническое graph evidence M4.8R2 недоступно.";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M48StaticOccupancyQualificationEvidence({
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
result: M48StaticOccupancyQualificationResult;
|
||||||
|
}) {
|
||||||
|
const [graph, setGraph] = useState<M47ReferenceGraphLabResult | null>(null);
|
||||||
|
const [cases, setCases] = useState<readonly M48StaticOccupancyCase[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
void Promise.all([
|
||||||
|
fetchM47ReferenceGraphLab({
|
||||||
|
resultId: result.referenceGraphLabResultId,
|
||||||
|
signal: controller.signal,
|
||||||
|
}),
|
||||||
|
fetchM48StaticOccupancyCases(result.resultId, { signal: controller.signal }),
|
||||||
|
])
|
||||||
|
.then(([nextGraph, nextCases]) => {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
if (nextCases.some((item) => item.sequence < 1 || item.sequence > nextGraph.frames.expected)) {
|
||||||
|
throw new Error("M4.8R2 anchor вышел за immutable timeline Canonical Reference Graph.");
|
||||||
|
}
|
||||||
|
setGraph(nextGraph);
|
||||||
|
setCases(nextCases);
|
||||||
|
})
|
||||||
|
.catch((caught: unknown) => {
|
||||||
|
if (!controller.signal.aborted) setError(message(caught));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!controller.signal.aborted) setLoading(false);
|
||||||
|
});
|
||||||
|
return () => controller.abort();
|
||||||
|
}, [result.referenceGraphLabResultId, result.resultId]);
|
||||||
|
|
||||||
|
const reviewAnchors = useMemo<readonly M4ReplayThreatReviewAnchor[]>(() => (
|
||||||
|
cases.map((item) => ({
|
||||||
|
id: item.anchorId,
|
||||||
|
sourceSequence: item.sequence - 1,
|
||||||
|
extentXyxyNormalized: item.extentXyxy,
|
||||||
|
matchedAtThreshold: item.candidateQualified,
|
||||||
|
}))
|
||||||
|
), [cases]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="l3-visual-audit__state" role="status">
|
||||||
|
<span className="busy-indicator" aria-hidden="true" />
|
||||||
|
<span>Открываем M4.7 graph и M4.8R2 static anchors</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (error || !graph) {
|
||||||
|
return (
|
||||||
|
<div className="l3-visual-audit__state" role="alert">
|
||||||
|
<Icon name="alert" size={18} />
|
||||||
|
<span>{error ?? "Canonical Reference Graph не связан с M4.8R2."}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<M4ReplayThreatVisual
|
||||||
|
resultId={graph.visual.resultId}
|
||||||
|
semantic={{ resultId: graph.semantic.resultId, taxonomy: graph.semantic.taxonomy }}
|
||||||
|
reviewAnchors={reviewAnchors}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
LaboratoryEvidence,
|
||||||
|
LaboratoryResultSummary,
|
||||||
|
LaboratorySummary,
|
||||||
|
LaboratoryWorkTemplate,
|
||||||
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
|
import type { M48StaticOccupancyQualificationResult } from "../../core/laboratory/m48StaticOccupancyQualification";
|
||||||
|
import { M48StaticOccupancyQualificationEvidence } from "./M48StaticOccupancyQualificationEvidence";
|
||||||
|
|
||||||
|
function percent(value: number): string {
|
||||||
|
return `${(value * 100).toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M48StaticOccupancyQualificationResultView({
|
||||||
|
rigLabel,
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
rigLabel: string;
|
||||||
|
result: M48StaticOccupancyQualificationResult;
|
||||||
|
}) {
|
||||||
|
const nearReady = result.decision.criticalNearCandidateReadyForShadow;
|
||||||
|
const status = nearReady
|
||||||
|
? "0–8 м: кандидат закрыл все assisted-якоря; готов к Worker shadow"
|
||||||
|
: "0–8 м: статическая occupancy ещё не закрыта";
|
||||||
|
return (
|
||||||
|
<LaboratoryWorkTemplate
|
||||||
|
summary={(
|
||||||
|
<LaboratorySummary
|
||||||
|
title="M4.8R2 · консервативная static occupancy"
|
||||||
|
description="RF-DETR не изменяется. Отдельный детерминированный прогон проверяет, удерживает ли current/rolling LiDAR низкие полусферы, столбы и другие безымянные ограничения, и оценивает уже вычисляемый CPU-признак low-step как additive occupied-only слой."
|
||||||
|
status={status}
|
||||||
|
statusTone={nearReady ? "success" : "warning"}
|
||||||
|
facts={[
|
||||||
|
{ label: "Конфигурация", value: `${rigLabel} RIGHT · Canonical Reference Graph · VIDEO/CAMERA/3D/PLAN/SEMANTICS` },
|
||||||
|
{ label: "Пайплайн", value: result.pipelineId },
|
||||||
|
{ label: "Прогон", value: `${result.runLabel} · immutable ${result.resultId}` },
|
||||||
|
{ label: "Нагрузка", value: "CPU-only geometry · 0 дополнительных detector inference" },
|
||||||
|
{ label: "Authority", value: "REPLAY-SIMULATED · free-space/commands/actuation OFF" },
|
||||||
|
]}
|
||||||
|
brief={{
|
||||||
|
question: "Не превратятся ли близкие полусферы, столбы и другие статические ограничения в пропуск только потому, что камера не знает их название?",
|
||||||
|
approach: `На ${result.metrics.operatorStaticAnchorCount} существующих operator-assisted static anchors принятую current/rolling occupancy сравнили с additive low-step evidence. Отдельно сохранены четыре канонических hemisphere-якоря кадров 1880/2584.`,
|
||||||
|
principalResult: `Принятый граф покрывает ${result.metrics.baselineQualifiedCount}/${result.metrics.operatorStaticAnchorCount}; additive кандидат — ${result.metrics.candidateQualifiedCount}/${result.metrics.operatorStaticAnchorCount}. В критической зоне 0–8 м: ${percent(result.metrics.criticalNearCandidateRecall)}.`,
|
||||||
|
limitation: "Это assisted diagnostic, а не independent truth. Кандидат пока не включён в Worker и может увеличить ложную занятость; требуются полный shadow-прогон, FPS и occupancy-volume gate.",
|
||||||
|
}}
|
||||||
|
method={{
|
||||||
|
completeness: "complete",
|
||||||
|
executionClass: "deterministic",
|
||||||
|
pipelineId: result.pipelineId,
|
||||||
|
components: [
|
||||||
|
{ kind: "source", name: result.referenceGraphLabResultId, version: "accepted M4.7", role: "immutable current/rolling occupied/unknown + threat graph", identitySha256: result.referenceGraphLabResultId.split("-").at(-1) ?? null },
|
||||||
|
{ kind: "source", name: result.smallStaticResultId, version: "M4.8R1 assisted seed", role: "static avoidance anchors · not independent truth", identitySha256: result.smallStaticResultId.split("-").at(-1) ?? null },
|
||||||
|
{ kind: "algorithm", name: "low-step occupied-only candidate", version: "v1", role: "additive CPU geometry; never clearing", identitySha256: result.resultId.split("-").at(-1) ?? null },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
evidence={(
|
||||||
|
<LaboratoryEvidence eyebrow="M4.8R2 VISUAL EVIDENCE · CANONICAL REFERENCE GRAPH" title="Те же CAMERA/3D/PLAN, 11 static anchors и неизменённый M4.7 timeline" kind="recorded-replay" resizable>
|
||||||
|
<M48StaticOccupancyQualificationEvidence result={result} />
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
|
<LaboratoryResultSummary
|
||||||
|
title="Что дал прогон: локализован лёгкий геометрический слой для близких препятствий"
|
||||||
|
status={status}
|
||||||
|
statusTone={nearReady ? "success" : "warning"}
|
||||||
|
metrics={[
|
||||||
|
{ label: "Current graph", value: `${result.metrics.baselineQualifiedCount}/${result.metrics.operatorStaticAnchorCount}`, hint: `${percent(result.metrics.criticalNearBaselineRecall)} в 0–8 м` },
|
||||||
|
{ label: "+ low-step candidate", value: `${result.metrics.candidateQualifiedCount}/${result.metrics.operatorStaticAnchorCount}`, hint: `${percent(result.metrics.criticalNearCandidateRecall)} в 0–8 м` },
|
||||||
|
{ label: "8–12 м", value: percent(result.metrics.approachCandidateRecall), hint: `${result.metrics.approachAnchorCount} assisted anchors · ещё не gate` },
|
||||||
|
{ label: "False free", value: result.metrics.falseFreeCount.toLocaleString("ru-RU"), hint: `${result.metrics.canonicalEngineeringAnchorCount}/${result.metrics.canonicalEngineeringAnchorCount} canonical anchors retained` },
|
||||||
|
]}
|
||||||
|
conclusion={{
|
||||||
|
proved: `На замороженном assisted seed additive low-step evidence поднял покрытие близких static anchors до ${percent(result.metrics.criticalNearCandidateRecall)} без дополнительной нейросети и без единого free-space утверждения.`,
|
||||||
|
notProved: "Не доказаны independent precision/recall, приемлемый рост консервативной occupancy, realtime Worker FPS после интеграции, физический clearance и collision safety.",
|
||||||
|
decision: nearReady
|
||||||
|
? "Интегрировать low-step только как occupied/unknown Worker shadow. Затем прогнать все 4 489 кадров и принять по FPS, росту компонентов, отсутствию capacity drops и повторному static-anchor gate."
|
||||||
|
: "Не интегрировать в Worker до исправления близких пропусков.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -77,6 +77,13 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
|||||||
experimentName: "M4.8 · small static passage regression",
|
experimentName: "M4.8 · small static passage regression",
|
||||||
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
|
variantName: "M4.8R1 · Worker 006 small-static assisted baseline",
|
||||||
},
|
},
|
||||||
|
"m48-static-occupancy-qualification": {
|
||||||
|
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||||
|
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Canonical Reference Graph`,
|
||||||
|
experimentId: "m48-static-occupancy-qualification",
|
||||||
|
experimentName: "M4.8 · conservative static occupancy qualification",
|
||||||
|
variantName: "M4.8R2 · current/rolling + low-step occupied-only candidate",
|
||||||
|
},
|
||||||
"m48s-fixed-class-detector": {
|
"m48s-fixed-class-detector": {
|
||||||
profileId: "rig-ravnoves-perception-gate-v1",
|
profileId: "rig-ravnoves-perception-gate-v1",
|
||||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
|
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function mergeResults(
|
|||||||
m47Graph: next.m47Graph ?? current.m47Graph,
|
m47Graph: next.m47Graph ?? current.m47Graph,
|
||||||
m48: next.m48 ?? current.m48,
|
m48: next.m48 ?? current.m48,
|
||||||
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
|
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
|
||||||
|
m48StaticOccupancy: next.m48StaticOccupancy ?? current.m48StaticOccupancy,
|
||||||
m48s: next.m48s ?? current.m48s,
|
m48s: next.m48s ?? current.m48s,
|
||||||
m48t: next.m48t ?? current.m48t,
|
m48t: next.m48t ?? current.m48t,
|
||||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||||
@@ -120,6 +121,7 @@ export function useAdvancedLaboratoryCatalog({
|
|||||||
"m47-reference-graph-shadow",
|
"m47-reference-graph-shadow",
|
||||||
"m48-object-centric-quality",
|
"m48-object-centric-quality",
|
||||||
"m48-small-static-passage-regression",
|
"m48-small-static-passage-regression",
|
||||||
|
"m48-static-occupancy-qualification",
|
||||||
].includes(selectedWorkId)
|
].includes(selectedWorkId)
|
||||||
&& !indexedResultId
|
&& !indexedResultId
|
||||||
) return;
|
) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "missioncore.laboratory-value-review-registry/v1",
|
"schema_version": "missioncore.laboratory-value-review-registry/v1",
|
||||||
"reviewed_at_utc": "2026-08-25T11:06:28Z",
|
"reviewed_at_utc": "2026-08-26T08:34:34Z",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"catalog_id": "e28-local-surface",
|
"catalog_id": "e28-local-surface",
|
||||||
@@ -247,6 +247,13 @@
|
|||||||
"lifecycle": "current",
|
"lifecycle": "current",
|
||||||
"visual_evidence": "available"
|
"visual_evidence": "available"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"catalog_id": "m48-static-occupancy-qualification",
|
||||||
|
"evidence_id": "m48-static-occupancy-qualification-568024554cff011332ff19ca4739f70555a6232c0607dec2a71be6db408ea69a",
|
||||||
|
"signal": "progress",
|
||||||
|
"lifecycle": "current",
|
||||||
|
"visual_evidence": "available"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"catalog_id": "m48s-fixed-class-detector",
|
"catalog_id": "m48s-fixed-class-detector",
|
||||||
"evidence_id": "m48s-fixed-class-detector-lab-4a901d811f53734540337f8d1f2c01666539aa0a9b7786f26d4f1f2e08cc858a",
|
"evidence_id": "m48s-fixed-class-detector-lab-4a901d811f53734540337f8d1f2c01666539aa0a9b7786f26d4f1f2e08cc858a",
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
|||||||
root / "config" / "laboratory-value-review.json"
|
root / "config" / "laboratory-value-review.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(registry.entries) == 37
|
assert len(registry.entries) == 38
|
||||||
assert {entry.catalog_id for entry in registry.entries} >= {
|
assert {entry.catalog_id for entry in registry.entries} >= {
|
||||||
"e28-local-surface",
|
"e28-local-surface",
|
||||||
"e46d-temporal-failure-audit",
|
"e46d-temporal-failure-audit",
|
||||||
@@ -88,6 +88,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
|
|||||||
"l31-pointpillars-ravnoves",
|
"l31-pointpillars-ravnoves",
|
||||||
"l34f-adjudicated-reference",
|
"l34f-adjudicated-reference",
|
||||||
"m4-replay-threat",
|
"m4-replay-threat",
|
||||||
|
"m48-static-occupancy-qualification",
|
||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user