feat(control-station): present static occupancy qualification
This commit is contained in:
@@ -42,12 +42,14 @@ import {
|
||||
fetchM48LifecycleResult,
|
||||
} from "./m48ObjectCentricQuality";
|
||||
import { fetchM48SmallStaticRegression } from "./m48SmallStaticRegression";
|
||||
import { fetchM48StaticOccupancyQualification } from "./m48StaticOccupancyQualification";
|
||||
import { fetchM48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||
import { fetchM48TRiskQualityResult } from "./m48tRiskQuality";
|
||||
|
||||
export type AdvancedLaboratoryWorkId =
|
||||
| "m48-object-centric-quality"
|
||||
| "m48-small-static-passage-regression"
|
||||
| "m48-static-occupancy-qualification"
|
||||
| "m48s-fixed-class-detector"
|
||||
| "m48t-risk-quality-temporal"
|
||||
| "m47-reference-graph-shadow"
|
||||
@@ -94,6 +96,7 @@ export interface AdvancedLaboratoryIndexItem {
|
||||
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
"m48-object-centric-quality",
|
||||
"m48-small-static-passage-regression",
|
||||
"m48-static-occupancy-qualification",
|
||||
"m48s-fixed-class-detector",
|
||||
"m48t-risk-quality-temporal",
|
||||
"m47-reference-graph-shadow",
|
||||
@@ -135,6 +138,7 @@ const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
|
||||
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
|
||||
"m48-object-centric-quality": "m48-object-quality-(?:pack|result)",
|
||||
"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",
|
||||
"m48t-risk-quality-temporal": "(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)",
|
||||
"m47-reference-graph-shadow": "m47-reference-graph-lab",
|
||||
@@ -184,6 +188,7 @@ export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
|
||||
m47Graph: null,
|
||||
m48: null,
|
||||
m48SmallStatic: null,
|
||||
m48StaticOccupancy: null,
|
||||
m48s: null,
|
||||
m48t: null,
|
||||
m4Threat: null,
|
||||
@@ -312,6 +317,7 @@ export function advancedLaboratoryResultAvailable(
|
||||
): boolean {
|
||||
return workId === "m48-object-centric-quality" ? results.m48 !== 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 === "m48t-risk-quality-temporal" ? results.m48t !== 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") {
|
||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8R1 regression identity не выбрана.");
|
||||
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") {
|
||||
if (!resultId) throw new AdvancedLaboratoryContractError("M4.8S LAB identity не выбрана.");
|
||||
results.m48s = await fetchM48SFixedClassDetectorResult(resultId, { fetcher, signal });
|
||||
|
||||
@@ -36,6 +36,7 @@ import type { M4ThreatReplayResult } from "./m4ReplayThreat";
|
||||
import type { M47ReferenceGraphLabResult } from "./m47ReferenceGraph";
|
||||
import type { M48AdvancedResult } from "./m48ObjectCentricQuality";
|
||||
import type { M48SmallStaticRegressionResult } from "./m48SmallStaticRegression";
|
||||
import type { M48StaticOccupancyQualificationResult } from "./m48StaticOccupancyQualification";
|
||||
import type { M48SFixedClassDetectorResult } from "./m48sFixedClassDetector";
|
||||
import type { M48TRiskQualityResult } from "./m48tRiskQuality";
|
||||
|
||||
@@ -43,6 +44,7 @@ export interface AdvancedLaboratoryResults {
|
||||
m47Graph: M47ReferenceGraphLabResult | null;
|
||||
m48: M48AdvancedResult | null;
|
||||
m48SmallStatic: M48SmallStaticRegressionResult | null;
|
||||
m48StaticOccupancy: M48StaticOccupancyQualificationResult | null;
|
||||
m48s: M48SFixedClassDetectorResult | null;
|
||||
m48t: M48TRiskQualityResult | null;
|
||||
m4Threat: M4ThreatReplayResult | null;
|
||||
|
||||
@@ -967,10 +967,9 @@ export async function fetchAdvancedLaboratoryResults({
|
||||
const e39 = settledCatalogValue(settled[7]);
|
||||
const e40 = settledCatalogValue(settled[8]);
|
||||
return {
|
||||
m47Graph: null, m48: null, m48SmallStatic: null, m48s: null, m48t: null, m4Threat: null,
|
||||
l3: null, l31: null,
|
||||
l32: null,
|
||||
l33: null,
|
||||
m47Graph: null, m48: null, m48SmallStatic: null, m48StaticOccupancy: null,
|
||||
m48s: null, m48t: null, m4Threat: null,
|
||||
l3: null, l31: null, l32: null, l33: null,
|
||||
e31,
|
||||
e32,
|
||||
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 { M48ObjectCentricQualityResultView } from "./M48ObjectCentricQualityResult";
|
||||
import { M48SmallStaticPassageRegressionResultView } from "./M48SmallStaticPassageRegressionResult";
|
||||
import { M48StaticOccupancyQualificationResultView } from "./M48StaticOccupancyQualificationResult";
|
||||
import { M48SFixedClassDetectorResultView } from "./M48SFixedClassDetectorResult";
|
||||
import { M48TRiskQualityResultView } from "./M48TRiskQualityResult";
|
||||
|
||||
@@ -94,6 +95,9 @@ export function AdvancedLaboratoryResult({
|
||||
if (workId === "m48-small-static-passage-regression" && 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) {
|
||||
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",
|
||||
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": {
|
||||
profileId: "rig-ravnoves-perception-gate-v1",
|
||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · RAVNOVES00 perception gate`,
|
||||
|
||||
@@ -21,6 +21,7 @@ function mergeResults(
|
||||
m47Graph: next.m47Graph ?? current.m47Graph,
|
||||
m48: next.m48 ?? current.m48,
|
||||
m48SmallStatic: next.m48SmallStatic ?? current.m48SmallStatic,
|
||||
m48StaticOccupancy: next.m48StaticOccupancy ?? current.m48StaticOccupancy,
|
||||
m48s: next.m48s ?? current.m48s,
|
||||
m48t: next.m48t ?? current.m48t,
|
||||
m4Threat: next.m4Threat ?? current.m4Threat,
|
||||
@@ -120,6 +121,7 @@ export function useAdvancedLaboratoryCatalog({
|
||||
"m47-reference-graph-shadow",
|
||||
"m48-object-centric-quality",
|
||||
"m48-small-static-passage-regression",
|
||||
"m48-static-occupancy-qualification",
|
||||
].includes(selectedWorkId)
|
||||
&& !indexedResultId
|
||||
) return;
|
||||
|
||||
Reference in New Issue
Block a user