diff --git a/apps/control-station/src/core/laboratory/advancedIndex.ts b/apps/control-station/src/core/laboratory/advancedIndex.ts index 1571268..2b8ac35 100644 --- a/apps/control-station/src/core/laboratory/advancedIndex.ts +++ b/apps/control-station/src/core/laboratory/advancedIndex.ts @@ -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> = { "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 }); diff --git a/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts b/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts index ff94ac3..92bd08e 100644 --- a/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts +++ b/apps/control-station/src/core/laboratory/advancedLaboratoryResults.ts @@ -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; diff --git a/apps/control-station/src/core/laboratory/advancedResults.ts b/apps/control-station/src/core/laboratory/advancedResults.ts index 605bb33..b0d7673 100644 --- a/apps/control-station/src/core/laboratory/advancedResults.ts +++ b/apps/control-station/src/core/laboratory/advancedResults.ts @@ -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, diff --git a/apps/control-station/src/core/laboratory/m48StaticOccupancyQualification.ts b/apps/control-station/src/core/laboratory/m48StaticOccupancyQualification.ts new file mode 100644 index 0000000..0c645f1 --- /dev/null +++ b/apps/control-station/src/core/laboratory/m48StaticOccupancyQualification.ts @@ -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 { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new M48StaticOccupancyContractError(`${label}: ожидался объект.`); + } + return value as Record; +} + +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(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 { + 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 { + 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"), + }; + }); +} diff --git a/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx b/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx index 9c4d072..18f1145 100644 --- a/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx +++ b/apps/control-station/src/workspaces/laboratory/AdvancedLaboratoryResult.tsx @@ -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 ; } + if (workId === "m48-static-occupancy-qualification" && results.m48StaticOccupancy) { + return ; + } if (workId === "m48s-fixed-class-detector" && results.m48s) { return ; } diff --git a/apps/control-station/src/workspaces/laboratory/M48StaticOccupancyQualificationEvidence.tsx b/apps/control-station/src/workspaces/laboratory/M48StaticOccupancyQualificationEvidence.tsx new file mode 100644 index 0000000..1620188 --- /dev/null +++ b/apps/control-station/src/workspaces/laboratory/M48StaticOccupancyQualificationEvidence.tsx @@ -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(null); + const [cases, setCases] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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(() => ( + cases.map((item) => ({ + id: item.anchorId, + sourceSequence: item.sequence - 1, + extentXyxyNormalized: item.extentXyxy, + matchedAtThreshold: item.candidateQualified, + })) + ), [cases]); + + if (loading) { + return ( +
+
+ ); + } + if (error || !graph) { + return ( +
+ + {error ?? "Canonical Reference Graph не связан с M4.8R2."} +
+ ); + } + return ( + + ); +} diff --git a/apps/control-station/src/workspaces/laboratory/M48StaticOccupancyQualificationResult.tsx b/apps/control-station/src/workspaces/laboratory/M48StaticOccupancyQualificationResult.tsx new file mode 100644 index 0000000..b924b98 --- /dev/null +++ b/apps/control-station/src/workspaces/laboratory/M48StaticOccupancyQualificationResult.tsx @@ -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 ( + + )} + evidence={( + + + + )} + result={( + + )} + /> + ); +} diff --git a/apps/control-station/src/workspaces/laboratory/laboratoryArchiveProfiles.ts b/apps/control-station/src/workspaces/laboratory/laboratoryArchiveProfiles.ts index 8e32baf..52b9bcb 100644 --- a/apps/control-station/src/workspaces/laboratory/laboratoryArchiveProfiles.ts +++ b/apps/control-station/src/workspaces/laboratory/laboratoryArchiveProfiles.ts @@ -77,6 +77,13 @@ const KNOWN_WORKS: Readonly `${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`, diff --git a/apps/control-station/src/workspaces/laboratory/useAdvancedLaboratoryCatalog.ts b/apps/control-station/src/workspaces/laboratory/useAdvancedLaboratoryCatalog.ts index e54c396..d0a8bd9 100644 --- a/apps/control-station/src/workspaces/laboratory/useAdvancedLaboratoryCatalog.ts +++ b/apps/control-station/src/workspaces/laboratory/useAdvancedLaboratoryCatalog.ts @@ -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; diff --git a/config/laboratory-value-review.json b/config/laboratory-value-review.json index 130674d..8cd0e2d 100644 --- a/config/laboratory-value-review.json +++ b/config/laboratory-value-review.json @@ -1,6 +1,6 @@ { "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": [ { "catalog_id": "e28-local-surface", @@ -247,6 +247,13 @@ "lifecycle": "current", "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", "evidence_id": "m48s-fixed-class-detector-lab-4a901d811f53734540337f8d1f2c01666539aa0a9b7786f26d4f1f2e08cc858a", diff --git a/tests/test_laboratory_value_review_registry.py b/tests/test_laboratory_value_review_registry.py index 424697d..a87ab5b 100644 --- a/tests/test_laboratory_value_review_registry.py +++ b/tests/test_laboratory_value_review_registry.py @@ -80,7 +80,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() -> 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} >= { "e28-local-surface", "e46d-temporal-failure-audit", @@ -88,6 +88,7 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() -> "l31-pointpillars-ravnoves", "l34f-adjudicated-reference", "m4-replay-threat", + "m48-static-occupancy-qualification", "m48s-fixed-class-detector", "m48t-risk-quality-temporal", }