|
|
|
@@ -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"),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|