Compare commits
6
Commits
385082c8ca
...
5983883ffe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5983883ffe | ||
|
|
6290c75210 | ||
|
|
3c81c39a1c | ||
|
|
932c5216dc | ||
|
|
f9a76fed0e | ||
|
|
c05db1fbe0 |
@@ -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,8 +138,9 @@ 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",
|
"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",
|
||||||
"m4-replay-threat": "m4-threat-replay",
|
"m4-replay-threat": "m4-threat-replay",
|
||||||
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
|
"l3-pointpillars-visual-audit": "l3-pointpillars-visual-audit",
|
||||||
@@ -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"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ export interface M48TReviewCase {
|
|||||||
sha256: string;
|
sha256: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface M48TRiskQualityResult {
|
export interface M48TLegacyRiskQualityResult {
|
||||||
|
variant: "legacy-coco-quality";
|
||||||
resultId: string;
|
resultId: string;
|
||||||
createdAtUtc: string;
|
createdAtUtc: string;
|
||||||
source: {
|
source: {
|
||||||
@@ -93,6 +94,90 @@ export interface M48TRiskQualityResult {
|
|||||||
limitations: readonly string[];
|
limitations: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type M48QRiskFamily = "person" | "animal" | "light-road-user" | "vehicle";
|
||||||
|
|
||||||
|
export interface M48QNativeProposal {
|
||||||
|
proposalId: string;
|
||||||
|
className: string;
|
||||||
|
riskFamily: M48QRiskFamily;
|
||||||
|
score: number;
|
||||||
|
boxXyxy: readonly [number, number, number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M48QNativeReviewCase {
|
||||||
|
caseId: string;
|
||||||
|
sequence: number;
|
||||||
|
frameId: string;
|
||||||
|
evidenceTimeNs: number;
|
||||||
|
imageUrl: string;
|
||||||
|
width: 800;
|
||||||
|
height: 600;
|
||||||
|
byteLength: number;
|
||||||
|
sha256: string;
|
||||||
|
selectionBuckets: readonly string[];
|
||||||
|
nativeDetectionCount: number;
|
||||||
|
legacyDetectionCount: number;
|
||||||
|
matchedDetectionCount: number;
|
||||||
|
proposals: readonly M48QNativeProposal[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M48QNativeRiskQualityResult {
|
||||||
|
variant: "native-risk-review";
|
||||||
|
resultId: string;
|
||||||
|
createdAtUtc: string;
|
||||||
|
source: {
|
||||||
|
sourceId: "RAVNOVES00";
|
||||||
|
frameCount: 4489;
|
||||||
|
width: 800;
|
||||||
|
height: 600;
|
||||||
|
geometricResampling: false;
|
||||||
|
};
|
||||||
|
candidate: {
|
||||||
|
providerId: string;
|
||||||
|
modelId: "rf_detr_large_native_kb4:1";
|
||||||
|
preprocessId: "raw-kb4-uint8-fused-mask-rgb-pad8-imagenet-trt/v0";
|
||||||
|
minimumScore: 0.25;
|
||||||
|
};
|
||||||
|
execution: {
|
||||||
|
effectiveWorldStateFps: number;
|
||||||
|
worldStateCompletionP95Ms: number;
|
||||||
|
detectorTotalP95Ms: number;
|
||||||
|
gpuUtilizationP95Percent: number;
|
||||||
|
gpuMemoryMaximumMib: number;
|
||||||
|
additionalInferencePasses: 0;
|
||||||
|
};
|
||||||
|
selection: {
|
||||||
|
caseCount: 24;
|
||||||
|
minimumSequenceSeparation: number;
|
||||||
|
bucketCoverage: Readonly<Record<string, number>>;
|
||||||
|
classCounts: Readonly<Record<string, number>>;
|
||||||
|
};
|
||||||
|
runtime: {
|
||||||
|
deliveryRatio: number;
|
||||||
|
integratedGatePassed: true;
|
||||||
|
operatingTargetGatePassed: true;
|
||||||
|
};
|
||||||
|
parity: {
|
||||||
|
precision: number;
|
||||||
|
recall: number;
|
||||||
|
meanIou: number;
|
||||||
|
};
|
||||||
|
acceptance: {
|
||||||
|
reviewReady: true;
|
||||||
|
independentQualityEvaluated: false;
|
||||||
|
semanticCandidateAccepted: false;
|
||||||
|
};
|
||||||
|
review: {
|
||||||
|
cases: readonly M48QNativeReviewCase[];
|
||||||
|
};
|
||||||
|
method: M48TLaboratoryMethod;
|
||||||
|
limitations: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type M48TRiskQualityResult =
|
||||||
|
| M48TLegacyRiskQualityResult
|
||||||
|
| M48QNativeRiskQualityResult;
|
||||||
|
|
||||||
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||||
|
|
||||||
export class M48TContractError extends Error {}
|
export class M48TContractError extends Error {}
|
||||||
@@ -173,9 +258,10 @@ function method(value: unknown): M48TLaboratoryMethod {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseResult(value: unknown, expectedResultId: string): M48TRiskQualityResult {
|
function parseLegacyResult(value: unknown, expectedResultId: string): M48TLegacyRiskQualityResult {
|
||||||
const raw = object(value, "M4.8T result");
|
const raw = object(value, "M4.8T result");
|
||||||
exact(raw.schema_version, "missioncore.m48t-risk-quality-temporal-view/v1", "M4.8T schema");
|
exact(raw.schema_version, "missioncore.m48t-risk-quality-temporal-view/v1", "M4.8T schema");
|
||||||
|
exact(raw.variant, "legacy-coco-quality", "M4.8T variant");
|
||||||
exact(raw.result_id, expectedResultId, "M4.8T identity");
|
exact(raw.result_id, expectedResultId, "M4.8T identity");
|
||||||
exact(raw.status, "complete-quality-gate-failed-temporal-invariant-passed", "M4.8T status");
|
exact(raw.status, "complete-quality-gate-failed-temporal-invariant-passed", "M4.8T status");
|
||||||
exact(raw.access, "read-only", "M4.8T access");
|
exact(raw.access, "read-only", "M4.8T access");
|
||||||
@@ -234,6 +320,7 @@ function parseResult(value: unknown, expectedResultId: string): M48TRiskQualityR
|
|||||||
for (const [key, count] of Object.entries(failures)) parsedFailures[key] = integer(count, `M4.8T failure ${key}`);
|
for (const [key, count] of Object.entries(failures)) parsedFailures[key] = integer(count, `M4.8T failure ${key}`);
|
||||||
const temporalConfiguration = object(configuration.temporal, "M4.8T temporal configuration");
|
const temporalConfiguration = object(configuration.temporal, "M4.8T temporal configuration");
|
||||||
return {
|
return {
|
||||||
|
variant: "legacy-coco-quality",
|
||||||
resultId: expectedResultId,
|
resultId: expectedResultId,
|
||||||
createdAtUtc: text(raw.created_at_utc, "M4.8T created at"),
|
createdAtUtc: text(raw.created_at_utc, "M4.8T created at"),
|
||||||
source: {
|
source: {
|
||||||
@@ -306,6 +393,207 @@ function parseResult(value: unknown, expectedResultId: string): M48TRiskQualityR
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function numberRecord(value: unknown, label: string): Readonly<Record<string, number>> {
|
||||||
|
const raw = object(value, label);
|
||||||
|
const parsed: Record<string, number> = {};
|
||||||
|
for (const [key, item] of Object.entries(raw)) parsed[key] = integer(item, `${label}.${key}`);
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nativeRiskFamily(value: unknown): M48QRiskFamily {
|
||||||
|
const parsed = text(value, "M4.8Q risk family");
|
||||||
|
if (!["person", "animal", "light-road-user", "vehicle"].includes(parsed)) {
|
||||||
|
throw new M48TContractError("M4.8Q risk family: неизвестное значение.");
|
||||||
|
}
|
||||||
|
return parsed as M48QRiskFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nativeProposal(value: unknown): M48QNativeProposal {
|
||||||
|
const raw = object(value, "M4.8Q proposal");
|
||||||
|
const coordinates = array(raw.box_xyxy, "M4.8Q proposal box").map((item) =>
|
||||||
|
number(item, "M4.8Q proposal coordinate")
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
coordinates.length !== 4
|
||||||
|
|| coordinates[0] >= coordinates[2]
|
||||||
|
|| coordinates[1] >= coordinates[3]
|
||||||
|
|| coordinates[2] > 800
|
||||||
|
|| coordinates[3] > 600
|
||||||
|
) {
|
||||||
|
throw new M48TContractError("M4.8Q proposal box: нарушена raw-raster геометрия.");
|
||||||
|
}
|
||||||
|
const score = number(raw.score, "M4.8Q proposal score");
|
||||||
|
if (score < 0.25 || score > 1) throw new M48TContractError("M4.8Q proposal score: нарушен threshold.");
|
||||||
|
return {
|
||||||
|
proposalId: text(raw.proposal_id, "M4.8Q proposal id"),
|
||||||
|
className: text(raw.class_name, "M4.8Q proposal class"),
|
||||||
|
riskFamily: nativeRiskFamily(raw.risk_family),
|
||||||
|
score,
|
||||||
|
boxXyxy: coordinates as [number, number, number, number],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNativeResult(value: unknown, expectedResultId: string): M48QNativeRiskQualityResult {
|
||||||
|
const raw = object(value, "M4.8Q result");
|
||||||
|
exact(raw.schema_version, "missioncore.m48q-native-risk-quality-view/v1", "M4.8Q schema");
|
||||||
|
exact(raw.variant, "native-risk-review", "M4.8Q variant");
|
||||||
|
exact(raw.result_id, expectedResultId, "M4.8Q identity");
|
||||||
|
exact(raw.status, "complete-review-ready-quality-not-adjudicated", "M4.8Q status");
|
||||||
|
exact(raw.access, "read-only", "M4.8Q access");
|
||||||
|
exact(raw.ground_truth, false, "M4.8Q ground truth");
|
||||||
|
|
||||||
|
const source = object(raw.source, "M4.8Q source");
|
||||||
|
exact(source.source_id, "RAVNOVES00", "M4.8Q source id");
|
||||||
|
exact(source.frame_count, 4489, "M4.8Q source frames");
|
||||||
|
exact(source.raster_width, 800, "M4.8Q source width");
|
||||||
|
exact(source.raster_height, 600, "M4.8Q source height");
|
||||||
|
exact(source.geometric_resampling, false, "M4.8Q source resampling");
|
||||||
|
exact(source.rectification, false, "M4.8Q source rectification");
|
||||||
|
exact(source.warp, false, "M4.8Q source warp");
|
||||||
|
|
||||||
|
const configuration = object(raw.configuration, "M4.8Q configuration");
|
||||||
|
const candidate = object(configuration.candidate, "M4.8Q candidate");
|
||||||
|
const execution = object(raw.execution, "M4.8Q execution");
|
||||||
|
const metrics = object(raw.metrics, "M4.8Q metrics");
|
||||||
|
const selection = object(metrics.selection, "M4.8Q selection");
|
||||||
|
const runtime = object(metrics.runtime, "M4.8Q runtime");
|
||||||
|
const parity = object(metrics.native_tensor_parity, "M4.8Q parity");
|
||||||
|
const acceptance = object(raw.acceptance, "M4.8Q acceptance");
|
||||||
|
exact(acceptance.review_ready, true, "M4.8Q review readiness");
|
||||||
|
exact(acceptance.integrated_runtime_gate_passed, true, "M4.8Q runtime gate");
|
||||||
|
exact(acceptance.independent_quality_evaluated, false, "M4.8Q quality state");
|
||||||
|
exact(acceptance.semantic_candidate_accepted, false, "M4.8Q candidate state");
|
||||||
|
exact(selection.case_count, 24, "M4.8Q selected cases");
|
||||||
|
|
||||||
|
const review = object(raw.review, "M4.8Q review");
|
||||||
|
const raster = object(review.source_raster, "M4.8Q review raster");
|
||||||
|
exact(raster.width, 800, "M4.8Q review width");
|
||||||
|
exact(raster.height, 600, "M4.8Q review height");
|
||||||
|
const overlay = object(review.overlay, "M4.8Q overlay");
|
||||||
|
exact(overlay.client_rendered, true, "M4.8Q client overlay");
|
||||||
|
exact(overlay.toggleable, true, "M4.8Q overlay toggle");
|
||||||
|
const cases = array(review.cases, "M4.8Q review cases").map((value) => {
|
||||||
|
const item = object(value, "M4.8Q review case");
|
||||||
|
const caseId = text(item.case_id, "M4.8Q case id");
|
||||||
|
if (!/^[0-9]{6}$/.test(caseId)) throw new M48TContractError("M4.8Q case identity нарушена.");
|
||||||
|
const sequence = integer(item.sequence, "M4.8Q case sequence");
|
||||||
|
exact(caseId, sequence.toString().padStart(6, "0"), "M4.8Q case/sequence identity");
|
||||||
|
exact(item.frame_id, `frame-${caseId}`, "M4.8Q frame identity");
|
||||||
|
exact(item.media_type, "image/jpeg", "M4.8Q case media");
|
||||||
|
exact(item.width, 800, "M4.8Q case width");
|
||||||
|
exact(item.height, 600, "M4.8Q case height");
|
||||||
|
exact(item.geometric_resampling, false, "M4.8Q case resampling");
|
||||||
|
const comparison = object(item.comparison, "M4.8Q comparison");
|
||||||
|
const proposals = array(item.proposals, "M4.8Q proposals").map(nativeProposal);
|
||||||
|
if (!proposals.length) throw new M48TContractError("M4.8Q proposals: пустой review case.");
|
||||||
|
return {
|
||||||
|
caseId,
|
||||||
|
sequence,
|
||||||
|
frameId: `frame-${caseId}`,
|
||||||
|
evidenceTimeNs: integer(item.evidence_time_ns, "M4.8Q evidence time"),
|
||||||
|
imageUrl: text(item.image_url, "M4.8Q image URL"),
|
||||||
|
width: 800 as const,
|
||||||
|
height: 600 as const,
|
||||||
|
byteLength: integer(item.byte_length, "M4.8Q image bytes"),
|
||||||
|
sha256: sha(item.sha256, "M4.8Q image SHA"),
|
||||||
|
selectionBuckets: array(item.selection_buckets, "M4.8Q selection buckets").map((bucket) =>
|
||||||
|
text(bucket, "M4.8Q selection bucket")
|
||||||
|
),
|
||||||
|
nativeDetectionCount: integer(comparison.native_detection_count, "M4.8Q native count"),
|
||||||
|
legacyDetectionCount: integer(comparison.legacy_704_detection_count, "M4.8Q legacy count"),
|
||||||
|
matchedDetectionCount: integer(
|
||||||
|
comparison.matched_detection_count_iou_at_least_0_5,
|
||||||
|
"M4.8Q matched count",
|
||||||
|
),
|
||||||
|
proposals,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (cases.length !== 24 || new Set(cases.map(({ caseId }) => caseId)).size !== 24) {
|
||||||
|
throw new M48TContractError("M4.8Q review catalog: нарушен размер.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
variant: "native-risk-review",
|
||||||
|
resultId: expectedResultId,
|
||||||
|
createdAtUtc: text(raw.created_at_utc, "M4.8Q created at"),
|
||||||
|
source: {
|
||||||
|
sourceId: "RAVNOVES00",
|
||||||
|
frameCount: 4489,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
geometricResampling: false,
|
||||||
|
},
|
||||||
|
candidate: {
|
||||||
|
providerId: text(candidate.provider_id, "M4.8Q provider"),
|
||||||
|
modelId: exact(candidate.model_id, "rf_detr_large_native_kb4:1", "M4.8Q model"),
|
||||||
|
preprocessId: exact(
|
||||||
|
candidate.preprocess_id,
|
||||||
|
"raw-kb4-uint8-fused-mask-rgb-pad8-imagenet-trt/v0",
|
||||||
|
"M4.8Q preprocess",
|
||||||
|
),
|
||||||
|
minimumScore: exact(candidate.minimum_score, 0.25, "M4.8Q threshold"),
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
effectiveWorldStateFps: number(execution.effective_world_state_fps, "M4.8Q FPS"),
|
||||||
|
worldStateCompletionP95Ms: number(
|
||||||
|
execution.world_state_completion_p95_ms,
|
||||||
|
"M4.8Q world-state p95",
|
||||||
|
),
|
||||||
|
detectorTotalP95Ms: number(execution.detector_total_p95_ms, "M4.8Q detector p95"),
|
||||||
|
gpuUtilizationP95Percent: number(
|
||||||
|
execution.gpu_utilization_p95_percent,
|
||||||
|
"M4.8Q GPU p95",
|
||||||
|
),
|
||||||
|
gpuMemoryMaximumMib: number(
|
||||||
|
execution.gpu_memory_used_maximum_mib,
|
||||||
|
"M4.8Q VRAM maximum",
|
||||||
|
),
|
||||||
|
additionalInferencePasses: exact(
|
||||||
|
execution.additional_inference_passes,
|
||||||
|
0,
|
||||||
|
"M4.8Q inference passes",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
caseCount: 24,
|
||||||
|
minimumSequenceSeparation: integer(
|
||||||
|
selection.minimum_sequence_separation,
|
||||||
|
"M4.8Q case separation",
|
||||||
|
),
|
||||||
|
bucketCoverage: numberRecord(selection.selected_bucket_coverage, "M4.8Q bucket coverage"),
|
||||||
|
classCounts: numberRecord(selection.selected_class_counts, "M4.8Q class counts"),
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
deliveryRatio: number(runtime.delivery_ratio, "M4.8Q delivery ratio"),
|
||||||
|
integratedGatePassed: exact(
|
||||||
|
runtime.integrated_runtime_gate_passed,
|
||||||
|
true,
|
||||||
|
"M4.8Q integrated gate",
|
||||||
|
),
|
||||||
|
operatingTargetGatePassed: exact(
|
||||||
|
runtime.operating_target_gate_passed,
|
||||||
|
true,
|
||||||
|
"M4.8Q target gate",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
parity: {
|
||||||
|
precision: number(parity.risk_detection_precision, "M4.8Q parity precision"),
|
||||||
|
recall: number(parity.risk_detection_recall, "M4.8Q parity recall"),
|
||||||
|
meanIou: number(parity.matched_mean_iou, "M4.8Q parity IoU"),
|
||||||
|
},
|
||||||
|
acceptance: {
|
||||||
|
reviewReady: true,
|
||||||
|
independentQualityEvaluated: false,
|
||||||
|
semanticCandidateAccepted: false,
|
||||||
|
},
|
||||||
|
review: { cases },
|
||||||
|
method: method(raw.method),
|
||||||
|
limitations: array(raw.limitations, "M4.8Q limitations").map((item) =>
|
||||||
|
text(item, "M4.8Q limitation")
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchM48TRiskQualityResult(
|
export async function fetchM48TRiskQualityResult(
|
||||||
resultId: string,
|
resultId: string,
|
||||||
{
|
{
|
||||||
@@ -313,7 +601,7 @@ export async function fetchM48TRiskQualityResult(
|
|||||||
signal,
|
signal,
|
||||||
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
||||||
): Promise<M48TRiskQualityResult> {
|
): Promise<M48TRiskQualityResult> {
|
||||||
if (!/^m48t-risk-quality-temporal-lab-[a-f0-9]{64}$/.test(resultId)) {
|
if (!/^(?:m48t-risk-quality-temporal-lab|m48q-native-risk-quality-lab)-[a-f0-9]{64}$/.test(resultId)) {
|
||||||
throw new M48TContractError("M4.8T result identity недопустима.");
|
throw new M48TContractError("M4.8T result identity недопустима.");
|
||||||
}
|
}
|
||||||
const response = await fetcher(`/api/v1/laboratory/m48t/risk-quality/results/${resultId}`, {
|
const response = await fetcher(`/api/v1/laboratory/m48t/risk-quality/results/${resultId}`, {
|
||||||
@@ -322,5 +610,9 @@ export async function fetchM48TRiskQualityResult(
|
|||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new M48TContractError(`M4.8T недоступен: HTTP ${response.status}.`);
|
if (!response.ok) throw new M48TContractError(`M4.8T недоступен: HTTP ${response.status}.`);
|
||||||
return parseResult(await response.json(), resultId);
|
const payload: unknown = await response.json();
|
||||||
|
const raw = object(payload, "M4.8T/M4.8Q result");
|
||||||
|
return raw.schema_version === "missioncore.m48q-native-risk-quality-view/v1"
|
||||||
|
? parseNativeResult(payload, resultId)
|
||||||
|
: parseLegacyResult(payload, resultId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 до исправления близких пропусков.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,11 @@ import {
|
|||||||
LaboratorySummary,
|
LaboratorySummary,
|
||||||
LaboratoryWorkTemplate,
|
LaboratoryWorkTemplate,
|
||||||
} from "../../components/laboratory/LaboratoryPresentation";
|
} from "../../components/laboratory/LaboratoryPresentation";
|
||||||
import type { M48TRiskQualityResult } from "../../core/laboratory/m48tRiskQuality";
|
import type {
|
||||||
|
M48QNativeRiskQualityResult,
|
||||||
|
M48TLegacyRiskQualityResult,
|
||||||
|
M48TRiskQualityResult,
|
||||||
|
} from "../../core/laboratory/m48tRiskQuality";
|
||||||
import { M48TRiskQualityVisual } from "./M48TRiskQualityVisual";
|
import { M48TRiskQualityVisual } from "./M48TRiskQualityVisual";
|
||||||
|
|
||||||
function percent(value: number, digits = 1): string {
|
function percent(value: number, digits = 1): string {
|
||||||
@@ -21,6 +25,19 @@ export function M48TRiskQualityResultView({
|
|||||||
}: {
|
}: {
|
||||||
rigLabel: string;
|
rigLabel: string;
|
||||||
result: M48TRiskQualityResult;
|
result: M48TRiskQualityResult;
|
||||||
|
}) {
|
||||||
|
if (result.variant === "native-risk-review") {
|
||||||
|
return <M48QNativeRiskQualityResultView rigLabel={rigLabel} result={result} />;
|
||||||
|
}
|
||||||
|
return <M48TLegacyRiskQualityResultView rigLabel={rigLabel} result={result} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function M48TLegacyRiskQualityResultView({
|
||||||
|
rigLabel,
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
rigLabel: string;
|
||||||
|
result: M48TLegacyRiskQualityResult;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<LaboratoryWorkTemplate
|
<LaboratoryWorkTemplate
|
||||||
@@ -77,3 +94,69 @@ export function M48TRiskQualityResultView({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function M48QNativeRiskQualityResultView({
|
||||||
|
rigLabel,
|
||||||
|
result,
|
||||||
|
}: {
|
||||||
|
rigLabel: string;
|
||||||
|
result: M48QNativeRiskQualityResult;
|
||||||
|
}) {
|
||||||
|
const classes = Object.entries(result.selection.classCounts)
|
||||||
|
.map(([name, count]) => `${name} ${count}`)
|
||||||
|
.join(" · ");
|
||||||
|
return (
|
||||||
|
<LaboratoryWorkTemplate
|
||||||
|
summary={(
|
||||||
|
<LaboratorySummary
|
||||||
|
title="M4.8Q · native raw-fisheye risk review"
|
||||||
|
description="Нативный RF-DETR прогнан внутри полного reference graph прямо на исходном KB4 raster 800×600: без rectification, warp, resize и обратного преобразования. Из immutable frame ledger детерминированно отобраны реальные risk-кейсы для операторской проверки в существующем M4.8 image-case instrument."
|
||||||
|
status="Runtime принят · 24 native cases готовы · quality ещё не adjudicated"
|
||||||
|
statusTone="success"
|
||||||
|
facts={[
|
||||||
|
{ label: "Source", value: `${rigLabel} · ${result.source.frameCount.toLocaleString("ru-RU")} raw frames · 800×600` },
|
||||||
|
{ label: "Candidate", value: `${result.candidate.modelId} · score ≥ ${decimal(result.candidate.minimumScore, 2)}` },
|
||||||
|
{ label: "Image path", value: "RAW KB4 → fused GPU graph · geometric resampling NO" },
|
||||||
|
{ label: "Authority", value: "SHADOW ONLY · independent quality NO · production NO" },
|
||||||
|
]}
|
||||||
|
brief={{
|
||||||
|
question: "Работает ли нативная risk-классификация на нашей исходной fisheye-картинке в realtime envelope, и какие реальные кадры надо проверить человеком?",
|
||||||
|
approach: `Полный ${result.source.frameCount.toLocaleString("ru-RU")}-кадровый native graph оставлен неизменным. Повторной инференс-сессии для лабы нет: 24 кадра выбраны из его hash-bound ledger по person, animal, light-road-user, vehicle, low-confidence, fisheye-edge и расхождениям с диагностической legacy 704 веткой.`,
|
||||||
|
principalResult: `${decimal(result.execution.effectiveWorldStateFps, 3)} FPS при detector p95 ${decimal(result.execution.detectorTotalP95Ms, 2)} ms и world-state p95 ${decimal(result.execution.worldStateCompletionP95Ms, 2)} ms. В review pack реально попали ${classes}.`,
|
||||||
|
limitation: "Это диагностическая выборка маршрута без независимой разметки. Наличие бокса и класса можно осмотреть, но precision/recall и корректность каждого класса этой фазой ещё не доказаны.",
|
||||||
|
}}
|
||||||
|
method={result.method}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
evidence={(
|
||||||
|
<LaboratoryEvidence
|
||||||
|
eyebrow="M4.8Q VISUAL EVIDENCE · NATIVE RAW KB4"
|
||||||
|
title="24 исходных fisheye-кадра с отключаемым client-side overlay"
|
||||||
|
kind="diagnostic-model"
|
||||||
|
resizable
|
||||||
|
>
|
||||||
|
<M48TRiskQualityVisual result={result} />
|
||||||
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
|
<LaboratoryResultSummary
|
||||||
|
title="Native runtime закрыт; следующий честный шаг — adjudication этих кейсов"
|
||||||
|
status="Review-ready · quality-not-adjudicated"
|
||||||
|
statusTone="success"
|
||||||
|
metrics={[
|
||||||
|
{ label: "Full graph", value: `${decimal(result.execution.effectiveWorldStateFps, 3)} FPS`, hint: `delivery ${percent(result.runtime.deliveryRatio, 2)} · 4 489/4 489` },
|
||||||
|
{ label: "Detector / world-state p95", value: `${decimal(result.execution.detectorTotalP95Ms, 2)} / ${decimal(result.execution.worldStateCompletionP95Ms, 2)} ms`, hint: "один native pass · additional inference 0" },
|
||||||
|
{ label: "GPU / VRAM peak", value: `${decimal(result.execution.gpuUtilizationP95Percent, 1)}% / ${decimal(result.execution.gpuMemoryMaximumMib / 1024, 2)} GiB`, hint: "Worker 006 · RTX 4090 envelope" },
|
||||||
|
{ label: "Native parity", value: `${percent(result.parity.precision, 2)} / ${percent(result.parity.recall, 2)}`, hint: `precision / recall · mean IoU ${percent(result.parity.meanIou, 2)}` },
|
||||||
|
{ label: "Review pack", value: `${result.selection.caseCount}/24 cases`, hint: "8 diagnostic buckets · raw 800×600" },
|
||||||
|
]}
|
||||||
|
conclusion={{
|
||||||
|
proved: `Полный reference graph доставил все ${result.source.frameCount.toLocaleString("ru-RU")} world states на ${decimal(result.execution.effectiveWorldStateFps, 3)} FPS. Детектор использовал ровно один native 800×600 KB4 pass без геометрического resampling. Hash-bound review содержит person, dog, bicycle, skateboard, car и truck; боксы рисуются поверх чистого кадра и отключаются кнопкой.`,
|
||||||
|
notProved: "Не доказаны route-domain precision/recall, истинность 24 классов и боксов, child/adult, поведение объектов, физическая track identity и безопасность навигации. Legacy 704 count delta остаётся только диагностикой, потому что старая ветка растягивала 4:3 raster.",
|
||||||
|
decision: "Не добавлять второй detector и не менять realtime graph. Использовать эти 24 кейса как вход существующего M4.8 review/correction workflow; только после независимой adjudication считать route-domain semantic quality.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,35 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Icon, IconButton } from "@nodedc/ui-react";
|
import { Icon, IconButton } from "@nodedc/ui-react";
|
||||||
|
|
||||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||||
import type { M48TRiskQualityResult } from "../../core/laboratory/m48tRiskQuality";
|
import { RecordedEvidenceImageScene } from "../../components/laboratory/RecordedEvidenceImageScene";
|
||||||
|
import type {
|
||||||
|
RecordedEvidenceBox,
|
||||||
|
RecordedEvidenceBoxTone,
|
||||||
|
} from "../../components/laboratory/RecordedEvidenceBoxOverlay";
|
||||||
|
import type {
|
||||||
|
M48QNativeRiskQualityResult,
|
||||||
|
M48QRiskFamily,
|
||||||
|
M48TLegacyRiskQualityResult,
|
||||||
|
M48TRiskQualityResult,
|
||||||
|
} from "../../core/laboratory/m48tRiskQuality";
|
||||||
|
|
||||||
export function M48TRiskQualityVisual({ result }: { result: M48TRiskQualityResult }) {
|
function navigateIndex(current: number, offset: -1 | 1, length: number): number {
|
||||||
|
return (current + offset + length) % length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function familyTone(family: M48QRiskFamily): RecordedEvidenceBoxTone {
|
||||||
|
if (family === "person" || family === "animal") return "danger";
|
||||||
|
if (family === "light-road-user") return "warning";
|
||||||
|
return "accent";
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegacyVisual({ result }: { result: M48TLegacyRiskQualityResult }) {
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const item = result.review.cases[index] ?? null;
|
const item = result.review.cases[index] ?? null;
|
||||||
const navigate = (offset: -1 | 1) => {
|
const navigate = (offset: -1 | 1) =>
|
||||||
setIndex((current) => (
|
setIndex((current) => navigateIndex(current, offset, result.review.cases.length));
|
||||||
current + offset + result.review.cases.length
|
|
||||||
) % result.review.cases.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="l3-visual-audit">
|
<div className="l3-visual-audit">
|
||||||
@@ -61,3 +78,85 @@ export function M48TRiskQualityVisual({ result }: { result: M48TRiskQualityResul
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NativeVisual({ result }: { result: M48QNativeRiskQualityResult }) {
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [boxesVisible, setBoxesVisible] = useState(true);
|
||||||
|
const item = result.review.cases[index] ?? null;
|
||||||
|
const boxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
|
||||||
|
if (!item || !boxesVisible) return [];
|
||||||
|
return item.proposals.map((proposal) => ({
|
||||||
|
boxXyxy: proposal.boxXyxy,
|
||||||
|
label: `${proposal.className} · ${proposal.score.toFixed(2)}`,
|
||||||
|
tone: familyTone(proposal.riskFamily),
|
||||||
|
}));
|
||||||
|
}, [boxesVisible, item]);
|
||||||
|
const navigate = (offset: -1 | 1) =>
|
||||||
|
setIndex((current) => navigateIndex(current, offset, result.review.cases.length));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="l3-visual-audit">
|
||||||
|
<LaboratoryEvidenceViewer
|
||||||
|
label="M4.8Q native raw-fisheye risk review"
|
||||||
|
mode="native"
|
||||||
|
modes={[{ value: "native", label: "NATIVE RF-DETR" }]}
|
||||||
|
expanded={expanded}
|
||||||
|
onModeChange={() => undefined}
|
||||||
|
onExpandedChange={setExpanded}
|
||||||
|
actions={(
|
||||||
|
<div className="l3-visual-audit__pagination">
|
||||||
|
<IconButton label="Предыдущий M4.8Q review case" onClick={() => navigate(-1)}>
|
||||||
|
<Icon name="chevron-left" size={16} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton label="Следующий M4.8Q review case" onClick={() => navigate(1)}>
|
||||||
|
<Icon name="chevron-right" size={16} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
trailingActions={(
|
||||||
|
<IconButton
|
||||||
|
label={boxesVisible ? "Скрыть native RF-DETR боксы" : "Показать native RF-DETR боксы"}
|
||||||
|
onClick={() => setBoxesVisible((visible) => !visible)}
|
||||||
|
>
|
||||||
|
<Icon name={boxesVisible ? "eye-off" : "eye"} size={16} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
overlay={item ? (
|
||||||
|
<div className="l3-visual-audit__overlay">
|
||||||
|
<div>
|
||||||
|
<span>RAVNOVES00 · raw KB4 800×600 · resampling NO</span>
|
||||||
|
<strong>
|
||||||
|
case {index + 1}/{result.review.cases.length} · frame {item.sequence} · {item.proposals.length} native boxes
|
||||||
|
</strong>
|
||||||
|
<small>
|
||||||
|
{item.selectionBuckets.join(" · ")} · native/legacy {item.nativeDetectionCount}/{item.legacyDetectionCount} (diagnostic only)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
>
|
||||||
|
{item ? (
|
||||||
|
<RecordedEvidenceImageScene
|
||||||
|
src={item.imageUrl}
|
||||||
|
imageWidth={item.width}
|
||||||
|
imageHeight={item.height}
|
||||||
|
boxes={boxes}
|
||||||
|
ariaLabel={`M4.8Q frame ${item.sequence}: ${boxes.length} visible native risk boxes`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="l3-visual-audit__state" role="alert">
|
||||||
|
<Icon name="alert" size={18} />
|
||||||
|
M4.8Q native visual evidence недоступно.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</LaboratoryEvidenceViewer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function M48TRiskQualityVisual({ result }: { result: M48TRiskQualityResult }) {
|
||||||
|
return result.variant === "native-risk-review"
|
||||||
|
? <NativeVisual result={result} />
|
||||||
|
: <LegacyVisual result={result} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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`,
|
||||||
@@ -86,10 +93,10 @@ const KNOWN_WORKS: Readonly<Record<Exclude<LaboratoryWorkId, `session:${string}`
|
|||||||
},
|
},
|
||||||
"m48t-risk-quality-temporal": {
|
"m48t-risk-quality-temporal": {
|
||||||
profileId: "rig-ravnoves-perception-gate-v1",
|
profileId: "rig-ravnoves-perception-gate-v1",
|
||||||
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · COCO quality + bounded temporal identity`,
|
profileName: (rigLabel) => `${rig(rigLabel)} RIGHT · Native raw-fisheye perception gate`,
|
||||||
experimentId: "m48t-risk-quality-temporal",
|
experimentId: "m48t-risk-quality-temporal",
|
||||||
experimentName: "RF-DETR independent semantic quality and temporal identity",
|
experimentName: "RF-DETR native risk review and temporal identity",
|
||||||
variantName: "M4.8T · COCO val2017 truth + RAVNOVES00 temporal shadow",
|
variantName: "M4.8Q · native raw KB4 review · quality not adjudicated",
|
||||||
},
|
},
|
||||||
"m47-reference-graph-shadow": {
|
"m47-reference-graph-shadow": {
|
||||||
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
profileId: "rig-dual-evidence-virtual-corridor-v1",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { after, before, test } from "node:test";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { createServer } from "vite";
|
||||||
|
|
||||||
|
let server;
|
||||||
|
let fetchM48TRiskQualityResult;
|
||||||
|
|
||||||
|
const resultId = `m48q-native-risk-quality-lab-${"a".repeat(64)}`;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
server = await createServer({
|
||||||
|
appType: "custom",
|
||||||
|
logLevel: "silent",
|
||||||
|
server: { middlewareMode: true },
|
||||||
|
});
|
||||||
|
({ fetchM48TRiskQualityResult } = await server.ssrLoadModule(
|
||||||
|
"/src/core/laboratory/m48tRiskQuality.ts",
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => server?.close());
|
||||||
|
|
||||||
|
function response(value) {
|
||||||
|
return { ok: true, status: 200, json: async () => value };
|
||||||
|
}
|
||||||
|
|
||||||
|
function payload() {
|
||||||
|
return {
|
||||||
|
schema_version: "missioncore.m48q-native-risk-quality-view/v1",
|
||||||
|
variant: "native-risk-review",
|
||||||
|
result_id: resultId,
|
||||||
|
created_at_utc: "2026-08-26T10:00:00Z",
|
||||||
|
status: "complete-review-ready-quality-not-adjudicated",
|
||||||
|
access: "read-only",
|
||||||
|
ground_truth: false,
|
||||||
|
source: {
|
||||||
|
source_id: "RAVNOVES00",
|
||||||
|
frame_count: 4489,
|
||||||
|
raster_width: 800,
|
||||||
|
raster_height: 600,
|
||||||
|
geometric_resampling: false,
|
||||||
|
rectification: false,
|
||||||
|
warp: false,
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
candidate: {
|
||||||
|
provider_id: "triton-rf-detr-large-coco-native-kb4-risk-fp16-shadow/v0",
|
||||||
|
model_id: "rf_detr_large_native_kb4:1",
|
||||||
|
preprocess_id: "raw-kb4-uint8-fused-mask-rgb-pad8-imagenet-trt/v0",
|
||||||
|
minimum_score: 0.25,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
method: {
|
||||||
|
schema_version: "missioncore.laboratory-method/v1",
|
||||||
|
completeness: "complete",
|
||||||
|
execution_class: "hybrid",
|
||||||
|
pipeline_id: "m48q-native-raw-fisheye-risk-case-review/v1",
|
||||||
|
components: [{
|
||||||
|
kind: "model",
|
||||||
|
name: "RF-DETR-L native KB4 TensorRT",
|
||||||
|
version: "rf_detr_large_native_kb4:1",
|
||||||
|
role: "risk proposals",
|
||||||
|
identity_sha256: "b".repeat(64),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
effective_world_state_fps: 11.84338,
|
||||||
|
world_state_completion_p95_ms: 47.940779,
|
||||||
|
detector_total_p95_ms: 21.19895,
|
||||||
|
gpu_utilization_p95_percent: 53,
|
||||||
|
gpu_memory_used_maximum_mib: 9718,
|
||||||
|
additional_inference_passes: 0,
|
||||||
|
},
|
||||||
|
metrics: {
|
||||||
|
selection: {
|
||||||
|
case_count: 24,
|
||||||
|
minimum_sequence_separation: 12,
|
||||||
|
selected_bucket_coverage: { person: 22, animal: 3 },
|
||||||
|
selected_class_counts: { person: 44, dog: 3 },
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
delivery_ratio: 1,
|
||||||
|
integrated_runtime_gate_passed: true,
|
||||||
|
operating_target_gate_passed: true,
|
||||||
|
},
|
||||||
|
native_tensor_parity: {
|
||||||
|
risk_detection_precision: 0.9887,
|
||||||
|
risk_detection_recall: 0.9831,
|
||||||
|
matched_mean_iou: 0.9882,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
acceptance: {
|
||||||
|
review_ready: true,
|
||||||
|
integrated_runtime_gate_passed: true,
|
||||||
|
independent_quality_evaluated: false,
|
||||||
|
semantic_candidate_accepted: false,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
source_raster: { width: 800, height: 600 },
|
||||||
|
overlay: { client_rendered: true, toggleable: true },
|
||||||
|
cases: Array.from({ length: 24 }, (_, index) => {
|
||||||
|
const caseId = String(index * 20 + 12).padStart(6, "0");
|
||||||
|
return {
|
||||||
|
case_id: caseId,
|
||||||
|
sequence: Number(caseId),
|
||||||
|
frame_id: `frame-${caseId}`,
|
||||||
|
evidence_time_ns: 35_000_000_000 + index * 1_000_000,
|
||||||
|
image_url: `/review/${caseId}.jpg`,
|
||||||
|
media_type: "image/jpeg",
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
byte_length: 1000 + index,
|
||||||
|
sha256: String(index.toString(16)).padStart(64, "0"),
|
||||||
|
geometric_resampling: false,
|
||||||
|
selection_buckets: [index % 2 ? "person" : "animal"],
|
||||||
|
comparison: {
|
||||||
|
native_detection_count: 1,
|
||||||
|
legacy_704_detection_count: 1,
|
||||||
|
matched_detection_count_iou_at_least_0_5: 1,
|
||||||
|
},
|
||||||
|
proposals: [{
|
||||||
|
proposal_id: `proposal-${index}-0`,
|
||||||
|
class_name: index % 2 ? "person" : "dog",
|
||||||
|
risk_family: index % 2 ? "person" : "animal",
|
||||||
|
score: 0.75,
|
||||||
|
box_xyxy: [10, 20, 100, 200],
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
limitations: ["No independent route truth."],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("M4.8Q parser accepts only the native 800x600 no-resampling review contract", async () => {
|
||||||
|
const result = await fetchM48TRiskQualityResult(resultId, {
|
||||||
|
fetcher: async () => response(payload()),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.variant, "native-risk-review");
|
||||||
|
assert.equal(result.review.cases.length, 24);
|
||||||
|
assert.equal(result.review.cases[0].width, 800);
|
||||||
|
assert.equal(result.review.cases[0].proposals[0].className, "dog");
|
||||||
|
assert.equal(result.execution.additionalInferencePasses, 0);
|
||||||
|
assert.equal(result.acceptance.independentQualityEvaluated, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("M4.8Q parser fails closed if raw-fisheye geometry is resampled", async () => {
|
||||||
|
const invalid = payload();
|
||||||
|
invalid.source.geometric_resampling = true;
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
fetchM48TRiskQualityResult(resultId, {
|
||||||
|
fetcher: async () => response(invalid),
|
||||||
|
}),
|
||||||
|
/source resampling/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("M4.8Q reuses the canonical image scene and viewer overlay toggle", async () => {
|
||||||
|
const source = await readFile(
|
||||||
|
new URL("../src/workspaces/laboratory/M48TRiskQualityVisual.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(source, /<LaboratoryEvidenceViewer/);
|
||||||
|
assert.match(source, /<RecordedEvidenceImageScene/);
|
||||||
|
assert.match(source, /name=\{boxesVisible \? "eye-off" : "eye"\}/);
|
||||||
|
assert.match(source, /raw KB4 800×600 · resampling NO/);
|
||||||
|
assert.doesNotMatch(source, /<canvas/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||||
|
"work_id": "m48-static-occupancy-qualification",
|
||||||
|
"evidence": {
|
||||||
|
"runtime_relative_root": "m48/static-occupancy-qualification-results",
|
||||||
|
"result_id_prefix": "m48-static-occupancy-qualification",
|
||||||
|
"document_name": "manifest.json",
|
||||||
|
"schema_version": "missioncore.m48-static-occupancy-qualification-result/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
"schema_version": "missioncore.laboratory-evidence-definition/v2",
|
||||||
"work_id": "m48t-risk-quality-temporal",
|
"work_id": "m48t-risk-quality-temporal",
|
||||||
"evidence": {
|
"evidence_lifecycle": [
|
||||||
|
{
|
||||||
|
"phase": "legacy-quality",
|
||||||
"runtime_relative_root": "m48t-risk-quality/lab-results",
|
"runtime_relative_root": "m48t-risk-quality/lab-results",
|
||||||
"result_id_prefix": "m48t-risk-quality-temporal-lab",
|
"result_id_prefix": "m48t-risk-quality-temporal-lab",
|
||||||
"document_name": "manifest.json",
|
"document_name": "manifest.json",
|
||||||
"schema_version": "missioncore.m48t-risk-quality-temporal-lab/v1"
|
"schema_version": "missioncore.m48t-risk-quality-temporal-lab/v1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": "result",
|
||||||
|
"runtime_relative_root": "m48t-risk-quality/native-lab-results",
|
||||||
|
"result_id_prefix": "m48q-native-risk-quality-lab",
|
||||||
|
"document_name": "manifest.json",
|
||||||
|
"schema_version": "missioncore.m48q-native-risk-quality-lab/v1"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "missioncore.laboratory-execution-registry/v1",
|
"schema_version": "missioncore.laboratory-execution-registry/v1",
|
||||||
"definitions": [
|
"definitions": [
|
||||||
|
{
|
||||||
|
"work_id": "m48-static-occupancy-qualification",
|
||||||
|
"lifecycle": "canonical",
|
||||||
|
"isolation": "core-adapter",
|
||||||
|
"adapter_id": "canonical.m48-static-occupancy-qualification/v1",
|
||||||
|
"input_roles": [
|
||||||
|
"repository_root",
|
||||||
|
"profile_path",
|
||||||
|
"m47_lab_root",
|
||||||
|
"graph_result_root",
|
||||||
|
"small_static_result_root"
|
||||||
|
],
|
||||||
|
"contracts": {
|
||||||
|
"source": "missioncore.m48-static-occupancy-source-set/v1",
|
||||||
|
"provider": "missioncore.m48-additive-low-step-occupancy/v1",
|
||||||
|
"graph": "missioncore.m48-static-occupancy-case/v1",
|
||||||
|
"run": "missioncore.laboratory-run/v1",
|
||||||
|
"evidence": "missioncore.m48-static-occupancy-qualification-result/v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"work_id": "m48-small-static-passage-regression",
|
"work_id": "m48-small-static-passage-regression",
|
||||||
"lifecycle": "canonical",
|
"lifecycle": "canonical",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -256,7 +263,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"catalog_id": "m48t-risk-quality-temporal",
|
"catalog_id": "m48t-risk-quality-temporal",
|
||||||
"evidence_id": "m48t-risk-quality-temporal-lab-ed5355fe0adb9b18942d75aff2362d79190b9e864f070ebe7a1c15fccbc0fbfb",
|
"evidence_id": "m48q-native-risk-quality-lab-e8961d4db8ffb751503b9646bc8eeebec6c80c948ce2f5b4fa5b89c471e673c1",
|
||||||
"signal": "progress",
|
"signal": "progress",
|
||||||
"lifecycle": "current",
|
"lifecycle": "current",
|
||||||
"visual_evidence": "available"
|
"visual_evidence": "available"
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.m48-static-occupancy-qualification-profile/v1",
|
||||||
|
"profile_id": "m48-conservative-static-occupancy/v1",
|
||||||
|
"pipeline_id": "m4-current-rolling-plus-step-static-occupancy/v1",
|
||||||
|
"experiment_id": "m48-static-occupancy-qualification/v1",
|
||||||
|
"human_lab_id": "M4.8",
|
||||||
|
"run_label": "M4.8R2",
|
||||||
|
"source": {
|
||||||
|
"source_id": "RAVNOVES00",
|
||||||
|
"source_session_id": "20260720T065719Z_viewer_live",
|
||||||
|
"m47_lab_result_id": "m47-reference-graph-lab-49678f0a7c628c7e991af0964fa57d005baa027d2d1eea19f38bbfe27ed39ce5",
|
||||||
|
"m47_graph_result_id": "m47-reference-graph-5f6a851cd655c7cf07c3025dacadbc188018b0afa97eda3a08802266e12da87d",
|
||||||
|
"m47_graph_frames_sha256": "d2cd53f8cff555410959600a79eb9a101aad4a8009ba87bdd0f3c5f122948071",
|
||||||
|
"small_static_result_id": "m48-small-static-passage-regression-3e3a2001f87fd3adcb736de52a65e42515044d83faa3515f892705b40915c084",
|
||||||
|
"small_static_anchors_sha256": "6a317aa75dde8204d5574a56166bebc9347e8797934dbc82f076a0abfdfaa95a"
|
||||||
|
},
|
||||||
|
"selection": {
|
||||||
|
"motion": "static",
|
||||||
|
"requires_avoidance_or_clearance": true,
|
||||||
|
"canonical_engineering_sequences": [1880, 2584],
|
||||||
|
"independent_truth": false
|
||||||
|
},
|
||||||
|
"distance_bands_m": {
|
||||||
|
"critical_near": [0.0, 8.0],
|
||||||
|
"approach": [8.0, 12.0]
|
||||||
|
},
|
||||||
|
"candidate": {
|
||||||
|
"point_sources": ["local-surface-occupied", "local-surface-step-candidate"],
|
||||||
|
"minimum_points": 2,
|
||||||
|
"minimum_voxels": 1,
|
||||||
|
"voxel_size_m": 0.35,
|
||||||
|
"depth_cluster_minimum_gap_m": 0.65,
|
||||||
|
"depth_cluster_gap_fraction": 0.08,
|
||||||
|
"spatial_cluster_radius_m": 0.75
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"minimum_critical_near_candidate_recall": 1.0,
|
||||||
|
"minimum_approach_candidate_recall": 0.95,
|
||||||
|
"minimum_canonical_engineering_recall": 1.0,
|
||||||
|
"maximum_false_free_count": 0
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"absence_of_points_means_free": false,
|
||||||
|
"absence_of_camera_detection_means_free": false,
|
||||||
|
"step_candidate_can_only_add_occupied_or_unknown": true,
|
||||||
|
"semantic_class_used": false,
|
||||||
|
"planner_authoritative_free_space_claimed": false
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"mode": "replay-simulated",
|
||||||
|
"physical_live": false,
|
||||||
|
"commands_enabled": false,
|
||||||
|
"actuation_allowed": false,
|
||||||
|
"navigation_or_safety_accepted": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.m48q-native-risk-case-mining-profile/v1",
|
||||||
|
"profile_id": "m48q-ravnoves00-native-raw-kb4-risk-review/v1",
|
||||||
|
"question": "Which real RAVNOVES00 raw-fisheye frames expose the native RF-DETR risk semantics that require operator review before independent route truth exists?",
|
||||||
|
"source": {
|
||||||
|
"source_id": "RAVNOVES00",
|
||||||
|
"frame_count": 4489,
|
||||||
|
"raster_width": 800,
|
||||||
|
"raster_height": 600,
|
||||||
|
"video_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||||
|
"geometric_resampling": false,
|
||||||
|
"rectification": false,
|
||||||
|
"warp": false
|
||||||
|
},
|
||||||
|
"candidate": {
|
||||||
|
"provider_id": "triton-rf-detr-large-coco-native-kb4-risk-fp16-shadow/v0",
|
||||||
|
"model_id": "rf_detr_large_native_kb4:1",
|
||||||
|
"preprocess_id": "raw-kb4-uint8-fused-mask-rgb-pad8-imagenet-trt/v0",
|
||||||
|
"engine_sha256": "b8a40b3580edff001ec9680de68707242294ff590ab296000fae371f1083f695",
|
||||||
|
"minimum_score": 0.25
|
||||||
|
},
|
||||||
|
"risk_families": {
|
||||||
|
"person": ["person"],
|
||||||
|
"animal": ["bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe"],
|
||||||
|
"light-road-user": ["bicycle", "motorcycle", "skateboard"],
|
||||||
|
"vehicle": ["car", "bus", "truck"]
|
||||||
|
},
|
||||||
|
"selection": {
|
||||||
|
"case_count": 24,
|
||||||
|
"minimum_sequence_separation": 12,
|
||||||
|
"low_confidence_maximum_score": 0.4,
|
||||||
|
"fisheye_edge_margin_fraction": 0.12,
|
||||||
|
"bucket_quotas": {
|
||||||
|
"person": 3,
|
||||||
|
"animal": 3,
|
||||||
|
"light-road-user": 3,
|
||||||
|
"vehicle": 3,
|
||||||
|
"low-confidence": 3,
|
||||||
|
"fisheye-edge": 3,
|
||||||
|
"native-fewer-than-legacy": 3,
|
||||||
|
"native-more-than-legacy": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"quality_evaluated": false,
|
||||||
|
"ground_truth": false,
|
||||||
|
"candidate_accepted": false,
|
||||||
|
"production_accepted": false,
|
||||||
|
"purpose": "bounded-native-risk-case-review"
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"ground_truth": false,
|
||||||
|
"candidate_accepted": false,
|
||||||
|
"commands_enabled": false,
|
||||||
|
"actuation_allowed": false,
|
||||||
|
"navigation_or_safety_accepted": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -912,6 +912,17 @@ authority.
|
|||||||
Exit: local collision-space quality and deadline gates pass in replay and
|
Exit: local collision-space quality and deadline gates pass in replay and
|
||||||
shadow. This still does not authorize control.
|
shadow. This still does not authorize control.
|
||||||
|
|
||||||
|
M4.8R2 now supplies the bounded pre-shadow qualification for the first item.
|
||||||
|
On eleven operator-assisted static passage anchors, the accepted current/rolling
|
||||||
|
graph covers `6/11`, while the additive sealed low-step signal covers `10/11`;
|
||||||
|
the critical `0–8 m` subset is `9/9`, four canonical engineering anchors remain
|
||||||
|
matched and false-free count is zero. The item remains unchecked because this
|
||||||
|
is not independent truth and the candidate is not yet part of the Worker
|
||||||
|
pipeline. Its next gate is a full `4,489`-frame occupied-only Worker 006 shadow
|
||||||
|
with FPS, latency, occupancy growth, component-count and capacity-drop bounds.
|
||||||
|
Dynamic decay, Nav2 publication and planner-authoritative free space remain
|
||||||
|
separate later work.
|
||||||
|
|
||||||
### L6 — alternative odometry/mapping
|
### L6 — alternative odometry/mapping
|
||||||
|
|
||||||
This stage starts only when a source supplies unregistered sensor scans. Add
|
This stage starts only when a source supplies unregistered sensor scans. Add
|
||||||
|
|||||||
@@ -179,6 +179,13 @@ only on the annotated frame unless a separately evidenced and reviewable
|
|||||||
tracking result exists; ordinary model proposals and graph layers continue on
|
tracking result exists; ordinary model proposals and graph layers continue on
|
||||||
their own full recorded timeline.
|
their own full recorded timeline.
|
||||||
|
|
||||||
|
M4.8R2 is the canonical static-occupancy example of this rule. Its report and
|
||||||
|
eleven typed anchors bind to the accepted M4.7 LAB identity, use the same
|
||||||
|
`VIDEO/CAMERA/3D/PLAN/SEMANTICS` timeline and add no renderer or transport. A
|
||||||
|
qualified anchor means the additive occupied-only candidate has LiDAR support
|
||||||
|
inside that exact reviewed camera extent; it does not turn the extent into a 3D
|
||||||
|
collider or grant clearance, free-space, navigation or safety authority.
|
||||||
|
|
||||||
A third perception instrument is a product-surface change. It requires explicit
|
A third perception instrument is a product-surface change. It requires explicit
|
||||||
product-owner agreement before implementation and cannot be introduced by a LAB
|
product-owner agreement before implementation and cannot be introduced by a LAB
|
||||||
adapter, result component or experiment configuration.
|
adapter, result component or experiment configuration.
|
||||||
|
|||||||
@@ -1445,6 +1445,65 @@ configuration, dataset release, metrics and ONNX identity; only then is a
|
|||||||
candidate loaded through the existing Triton seam and measured by a new
|
candidate loaded through the existing Triton seam and measured by a new
|
||||||
append-only M4.8R run plus an independent validation gate.
|
append-only M4.8R run plus an independent validation gate.
|
||||||
|
|
||||||
|
### 2026-08-26 — M4.8Q native raw-fisheye risk review ready
|
||||||
|
|
||||||
|
The terminal phase of the existing `m48t-risk-quality-temporal` work is now the
|
||||||
|
content-addressed M4.8Q result
|
||||||
|
`m48q-native-risk-quality-lab-e8961d4db8ffb751503b9646bc8eeebec6c80c948ce2f5b4fa5b89c471e673c1`.
|
||||||
|
It replaces neither the historical M4.8T evidence nor either admitted M4 viewer.
|
||||||
|
The result projects `24` deterministic diagnostic cases into the existing M4.8
|
||||||
|
image-case review instrument, with source-pixel boxes rendered by the client and
|
||||||
|
an explicit clean-image toggle. The JPEG review derivatives retain the exact
|
||||||
|
`800×600` raster geometry; rectification, warp and geometric resampling are all
|
||||||
|
false. Boxes are not baked into the images.
|
||||||
|
|
||||||
|
Worker 006 selected the cases from the immutable full native reference-graph
|
||||||
|
ledger without another inference pass. The selection covers person, animal,
|
||||||
|
light-road-user, vehicle, low-confidence, fisheye-edge and both directions of
|
||||||
|
native-versus-legacy count divergence, with at least 12 source frames between
|
||||||
|
selected cases. The source graph delivered all `4,489/4,489` frames at
|
||||||
|
`11.84338 FPS`; detector p95 was `21.19895 ms`, world-state completion p95 was
|
||||||
|
`47.940779 ms`, GPU utilization p95 was `53%`, and maximum used GPU memory was
|
||||||
|
`9,718 MiB`. The isolated case-mining container was removed after execution and
|
||||||
|
the canonical Triton predecessor was verified healthy and unchanged.
|
||||||
|
|
||||||
|
This closes evidence preparation, not semantic quality. The cases are
|
||||||
|
diagnostic samples rather than independent route truth; native-versus-legacy
|
||||||
|
count differences are not a verdict because the legacy `704×704` path stretches
|
||||||
|
the raw 4:3 image. Accordingly `review_ready=true`, while ground truth,
|
||||||
|
candidate acceptance, production acceptance, navigation, safety, commands and
|
||||||
|
actuation authority remain false. The next action is operator adjudication in
|
||||||
|
the existing M4.8 review instrument.
|
||||||
|
|
||||||
|
### 2026-08-26 — M4.8R2 conservative static-occupancy qualification
|
||||||
|
|
||||||
|
M4.8R2 moves the small-static problem out of semantic detector tuning and into
|
||||||
|
the existing LiDAR occupancy boundary. RF-DETR, native fisheye input and its
|
||||||
|
risk taxonomy are unchanged. The deterministic qualification consumes the
|
||||||
|
accepted M4.7 current/rolling graph plus the frozen M4.8R1 operator-assisted
|
||||||
|
static anchors. It evaluates one already sealed local-surface signal,
|
||||||
|
`point_step_candidate`, only as additive occupied-or-unknown evidence. It never
|
||||||
|
clears a cell, claims free space, starts detector inference or uses the GPU.
|
||||||
|
|
||||||
|
Immutable result
|
||||||
|
`m48-static-occupancy-qualification-568024554cff011332ff19ca4739f70555a6232c0607dec2a71be6db408ea69a`
|
||||||
|
contains `11` static passage anchors. The accepted graph qualifies `6/11`; the
|
||||||
|
additive low-step candidate qualifies `10/11`. In the critical `0–8 m` band the
|
||||||
|
change is `6/9 → 9/9`; in the `8–12 m` approach band it is `0/2 → 1/2`. All
|
||||||
|
four existing canonical engineering anchors remain matched and the result
|
||||||
|
contains zero false-free claims. One approach anchor remains explicitly
|
||||||
|
`unresolved-unknown-never-free`.
|
||||||
|
|
||||||
|
This is candidate-visible development evidence, not independent truth or a
|
||||||
|
production cutover. The result accepts only a bounded next action: integrate
|
||||||
|
the low-step contribution as an occupied-only Worker shadow and replay all
|
||||||
|
`4,489` frames on Worker 006 while measuring effective FPS, stage latency,
|
||||||
|
occupancy/component growth, capacity drops and the same static anchors. The LAB
|
||||||
|
reuses the M4.7 `VIDEO/CAMERA/3D/PLAN/SEMANTICS` viewer and projects the eleven
|
||||||
|
anchors onto its immutable timeline; no third perception instrument exists.
|
||||||
|
Navigation, commands, actuation, physical clearance and collision-safety
|
||||||
|
authority remain false.
|
||||||
|
|
||||||
## Implementation order
|
## Implementation order
|
||||||
|
|
||||||
The implementation sequence is intentionally strict:
|
The implementation sequence is intentionally strict:
|
||||||
|
|||||||
@@ -0,0 +1,677 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Mine bounded native raw-fisheye risk cases from immutable Worker 006 ledgers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
from collections.abc import Iterable, Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
PROFILE_SCHEMA: Final = "missioncore.m48q-native-risk-case-mining-profile/v1"
|
||||||
|
RESULT_SCHEMA: Final = "missioncore.m48q-native-risk-case-mining-result/v1"
|
||||||
|
CASE_SCHEMA: Final = "missioncore.m48q-native-risk-review-case/v1"
|
||||||
|
GRAPH_FRAME_SCHEMA: Final = "missioncore.m48s-reference-graph-frame-evidence/v1"
|
||||||
|
COMPARISON_FRAME_SCHEMA: Final = "missioncore.m48n-native-vs-704-frame/v0"
|
||||||
|
FALSE_AUTHORITY: Final = {
|
||||||
|
"ground_truth": False,
|
||||||
|
"candidate_accepted": False,
|
||||||
|
"commands_enabled": False,
|
||||||
|
"actuation_allowed": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class M48QCaseMiningError(RuntimeError):
|
||||||
|
"""Raised when immutable native review evidence is inconsistent."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Proposal:
|
||||||
|
proposal_id: str
|
||||||
|
class_name: str
|
||||||
|
risk_family: str
|
||||||
|
score: float
|
||||||
|
box_xyxy: tuple[float, float, float, float]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class FrameCandidate:
|
||||||
|
sequence: int
|
||||||
|
frame_id: str
|
||||||
|
evidence_time_ns: int
|
||||||
|
proposals: tuple[Proposal, ...]
|
||||||
|
native_count: int
|
||||||
|
legacy_count: int
|
||||||
|
matched_count: int
|
||||||
|
buckets: frozenset[str]
|
||||||
|
|
||||||
|
|
||||||
|
def canonical_json(value: object) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
value,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_path(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _read_object(path: Path) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
value: object = json.loads(path.read_text("utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise M48QCaseMiningError(f"cannot read JSON object: {path.name}") from exc
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M48QCaseMiningError(f"JSON evidence is not an object: {path.name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_hash(path: Path, expected: str, label: str) -> str:
|
||||||
|
actual = sha256_path(path)
|
||||||
|
if actual != expected:
|
||||||
|
raise M48QCaseMiningError(f"{label} SHA-256 changed: {actual}")
|
||||||
|
return actual
|
||||||
|
|
||||||
|
|
||||||
|
def _risk_family_map(profile: Mapping[str, Any]) -> dict[str, str]:
|
||||||
|
raw = profile.get("risk_families")
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise M48QCaseMiningError("risk family profile is invalid")
|
||||||
|
result: dict[str, str] = {}
|
||||||
|
for family, classes in raw.items():
|
||||||
|
if (
|
||||||
|
family not in {"person", "animal", "light-road-user", "vehicle"}
|
||||||
|
or not isinstance(classes, list)
|
||||||
|
or not classes
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("risk family profile changed")
|
||||||
|
for class_name in classes:
|
||||||
|
if not isinstance(class_name, str) or class_name in result:
|
||||||
|
raise M48QCaseMiningError("risk classes are invalid or duplicated")
|
||||||
|
result[class_name] = family
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _proposal(
|
||||||
|
value: object,
|
||||||
|
*,
|
||||||
|
sequence: int,
|
||||||
|
profile: Mapping[str, Any],
|
||||||
|
families: Mapping[str, str],
|
||||||
|
) -> Proposal:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M48QCaseMiningError("detector proposal is not an object")
|
||||||
|
candidate = profile["candidate"]
|
||||||
|
expected_frame_id = f"frame-{sequence:06d}"
|
||||||
|
if (
|
||||||
|
value.get("frame_id") != expected_frame_id
|
||||||
|
or value.get("provider_id") != candidate["provider_id"]
|
||||||
|
or value.get("model_id") != candidate["model_id"]
|
||||||
|
or value.get("preprocess_id") != candidate["preprocess_id"]
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("native detector proposal identity changed")
|
||||||
|
proposal_id = value.get("proposal_id")
|
||||||
|
class_name = value.get("semantic_hint")
|
||||||
|
score = value.get("objectness")
|
||||||
|
region = value.get("region")
|
||||||
|
if (
|
||||||
|
not isinstance(proposal_id, str)
|
||||||
|
or not isinstance(class_name, str)
|
||||||
|
or class_name not in families
|
||||||
|
or not isinstance(score, (float, int))
|
||||||
|
or isinstance(score, bool)
|
||||||
|
or not math.isfinite(float(score))
|
||||||
|
or float(score) < candidate["minimum_score"]
|
||||||
|
or not isinstance(region, dict)
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("native detector proposal contract changed")
|
||||||
|
coordinates = tuple(region.get(key) for key in ("x_min", "y_min", "x_max", "y_max"))
|
||||||
|
if not all(
|
||||||
|
isinstance(item, (float, int)) and not isinstance(item, bool) and math.isfinite(float(item))
|
||||||
|
for item in coordinates
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("native detector box is invalid")
|
||||||
|
left, top, right, bottom = (float(item) for item in coordinates)
|
||||||
|
source = profile["source"]
|
||||||
|
if not (
|
||||||
|
0 <= left < right <= source["raster_width"] and 0 <= top < bottom <= source["raster_height"]
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("native detector box escaped the raw source raster")
|
||||||
|
return Proposal(
|
||||||
|
proposal_id=proposal_id,
|
||||||
|
class_name=class_name,
|
||||||
|
risk_family=families[class_name],
|
||||||
|
score=float(score),
|
||||||
|
box_xyxy=(left, top, right, bottom),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_comparison_rows(path: Path, expected_count: int) -> dict[int, tuple[int, int, int]]:
|
||||||
|
rows: dict[int, tuple[int, int, int]] = {}
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8") as stream:
|
||||||
|
for line_number, line in enumerate(stream, start=1):
|
||||||
|
value = json.loads(line)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M48QCaseMiningError("comparison frame is not an object")
|
||||||
|
if value.get("schema_version") not in {None, COMPARISON_FRAME_SCHEMA}:
|
||||||
|
raise M48QCaseMiningError("comparison frame schema changed")
|
||||||
|
sequence = value.get("frame_index")
|
||||||
|
counts = tuple(
|
||||||
|
value.get(key)
|
||||||
|
for key in (
|
||||||
|
"native_detection_count",
|
||||||
|
"baseline_detection_count",
|
||||||
|
"matched_detection_count_iou_at_least_0_5",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not isinstance(sequence, int)
|
||||||
|
or isinstance(sequence, bool)
|
||||||
|
or sequence in rows
|
||||||
|
or not all(
|
||||||
|
isinstance(item, int) and not isinstance(item, bool) and item >= 0
|
||||||
|
for item in counts
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError(f"comparison frame {line_number} is invalid")
|
||||||
|
rows[sequence] = counts
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise M48QCaseMiningError("comparison frame ledger cannot be read") from exc
|
||||||
|
if set(rows) != set(range(expected_count)):
|
||||||
|
raise M48QCaseMiningError("comparison frame ledger is incomplete")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _case_buckets(
|
||||||
|
proposals: Sequence[Proposal],
|
||||||
|
*,
|
||||||
|
native_count: int,
|
||||||
|
legacy_count: int,
|
||||||
|
profile: Mapping[str, Any],
|
||||||
|
) -> frozenset[str]:
|
||||||
|
selection = profile["selection"]
|
||||||
|
buckets = {item.risk_family for item in proposals}
|
||||||
|
if any(item.score <= selection["low_confidence_maximum_score"] for item in proposals):
|
||||||
|
buckets.add("low-confidence")
|
||||||
|
width = profile["source"]["raster_width"]
|
||||||
|
height = profile["source"]["raster_height"]
|
||||||
|
margin = selection["fisheye_edge_margin_fraction"]
|
||||||
|
if any(
|
||||||
|
item.box_xyxy[0] <= width * margin
|
||||||
|
or item.box_xyxy[2] >= width * (1 - margin)
|
||||||
|
or item.box_xyxy[1] <= height * margin
|
||||||
|
or item.box_xyxy[3] >= height * (1 - margin)
|
||||||
|
for item in proposals
|
||||||
|
):
|
||||||
|
buckets.add("fisheye-edge")
|
||||||
|
if native_count < legacy_count:
|
||||||
|
buckets.add("native-fewer-than-legacy")
|
||||||
|
elif native_count > legacy_count:
|
||||||
|
buckets.add("native-more-than-legacy")
|
||||||
|
return frozenset(buckets)
|
||||||
|
|
||||||
|
|
||||||
|
def load_graph_candidates(
|
||||||
|
path: Path,
|
||||||
|
*,
|
||||||
|
profile: Mapping[str, Any],
|
||||||
|
comparisons: Mapping[int, tuple[int, int, int]],
|
||||||
|
) -> list[FrameCandidate]:
|
||||||
|
families = _risk_family_map(profile)
|
||||||
|
expected_count = profile["source"]["frame_count"]
|
||||||
|
candidates: list[FrameCandidate] = []
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8") as stream:
|
||||||
|
for sequence, line in enumerate(stream):
|
||||||
|
value = json.loads(line)
|
||||||
|
if not isinstance(value, dict) or value.get("schema_version") != GRAPH_FRAME_SCHEMA:
|
||||||
|
raise M48QCaseMiningError("graph frame schema changed")
|
||||||
|
source = value.get("source_envelope")
|
||||||
|
raw_proposals = value.get("detector_proposals")
|
||||||
|
if (
|
||||||
|
not isinstance(source, dict)
|
||||||
|
or source.get("sequence") != sequence
|
||||||
|
or source.get("frame_id") != f"frame-{sequence:06d}"
|
||||||
|
or not isinstance(raw_proposals, list)
|
||||||
|
or value.get("authority") != FALSE_AUTHORITY
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("graph frame identity or authority changed")
|
||||||
|
timestamps = source.get("timestamps")
|
||||||
|
evidence_time_ns = (
|
||||||
|
timestamps.get("source_ns") if isinstance(timestamps, dict) else None
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not isinstance(evidence_time_ns, int)
|
||||||
|
or isinstance(evidence_time_ns, bool)
|
||||||
|
or evidence_time_ns < 0
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("graph frame evidence time is invalid")
|
||||||
|
proposals = tuple(
|
||||||
|
_proposal(item, sequence=sequence, profile=profile, families=families)
|
||||||
|
for item in raw_proposals
|
||||||
|
)
|
||||||
|
native_count, legacy_count, matched_count = comparisons[sequence]
|
||||||
|
if len(proposals) != native_count:
|
||||||
|
raise M48QCaseMiningError("native graph/comparison detector count changed")
|
||||||
|
buckets = _case_buckets(
|
||||||
|
proposals,
|
||||||
|
native_count=native_count,
|
||||||
|
legacy_count=legacy_count,
|
||||||
|
profile=profile,
|
||||||
|
)
|
||||||
|
candidates.append(
|
||||||
|
FrameCandidate(
|
||||||
|
sequence=sequence,
|
||||||
|
frame_id=source["frame_id"],
|
||||||
|
evidence_time_ns=evidence_time_ns,
|
||||||
|
proposals=proposals,
|
||||||
|
native_count=native_count,
|
||||||
|
legacy_count=legacy_count,
|
||||||
|
matched_count=matched_count,
|
||||||
|
buckets=buckets,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise M48QCaseMiningError("graph frame ledger cannot be read") from exc
|
||||||
|
if len(candidates) != expected_count:
|
||||||
|
raise M48QCaseMiningError("graph frame ledger is incomplete")
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _quantile_order(items: Sequence[FrameCandidate]) -> Iterable[FrameCandidate]:
|
||||||
|
if not items:
|
||||||
|
return ()
|
||||||
|
indexes: list[int] = []
|
||||||
|
left = 0
|
||||||
|
right = len(items) - 1
|
||||||
|
while left <= right:
|
||||||
|
middle = (left + right) // 2
|
||||||
|
indexes.append(middle)
|
||||||
|
if middle - left > 0:
|
||||||
|
indexes.append((left + middle - 1) // 2)
|
||||||
|
if right - middle > 0:
|
||||||
|
indexes.append((middle + 1 + right) // 2)
|
||||||
|
left += 1
|
||||||
|
right -= 1
|
||||||
|
seen: set[int] = set()
|
||||||
|
return (items[index] for index in indexes if not (index in seen or seen.add(index)))
|
||||||
|
|
||||||
|
|
||||||
|
def select_cases(
|
||||||
|
candidates: Sequence[FrameCandidate],
|
||||||
|
*,
|
||||||
|
bucket_quotas: Mapping[str, int],
|
||||||
|
minimum_sequence_separation: int,
|
||||||
|
) -> list[FrameCandidate]:
|
||||||
|
selected: list[FrameCandidate] = []
|
||||||
|
selected_sequences: set[int] = set()
|
||||||
|
for bucket, quota in bucket_quotas.items():
|
||||||
|
eligible = [item for item in candidates if bucket in item.buckets]
|
||||||
|
admitted = 0
|
||||||
|
for item in _quantile_order(eligible):
|
||||||
|
if item.sequence in selected_sequences:
|
||||||
|
continue
|
||||||
|
if any(
|
||||||
|
abs(item.sequence - prior.sequence) < minimum_sequence_separation
|
||||||
|
for prior in selected
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
selected.append(item)
|
||||||
|
selected_sequences.add(item.sequence)
|
||||||
|
admitted += 1
|
||||||
|
if admitted == quota:
|
||||||
|
break
|
||||||
|
if admitted != quota:
|
||||||
|
raise M48QCaseMiningError(
|
||||||
|
f"selection bucket {bucket} produced {admitted}/{quota} separated cases"
|
||||||
|
)
|
||||||
|
expected = sum(bucket_quotas.values())
|
||||||
|
if len(selected) != expected:
|
||||||
|
raise M48QCaseMiningError("selected case count changed")
|
||||||
|
return sorted(selected, key=lambda item: item.sequence)
|
||||||
|
|
||||||
|
|
||||||
|
def render_selected_frames(
|
||||||
|
*,
|
||||||
|
video_path: Path,
|
||||||
|
selected: Sequence[FrameCandidate],
|
||||||
|
cases_root: Path,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
) -> dict[int, dict[str, object]]:
|
||||||
|
import av
|
||||||
|
from av.error import FFmpegError
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
wanted = {item.sequence for item in selected}
|
||||||
|
rendered: dict[int, dict[str, object]] = {}
|
||||||
|
cases_root.mkdir(mode=0o700, parents=True, exist_ok=False)
|
||||||
|
try:
|
||||||
|
with av.open(str(video_path), mode="r") as container:
|
||||||
|
streams = list(container.streams.video)
|
||||||
|
if len(streams) != 1:
|
||||||
|
raise M48QCaseMiningError("RAVNOVES00 video stream contract changed")
|
||||||
|
for sequence, frame in enumerate(container.decode(streams[0])):
|
||||||
|
if sequence not in wanted:
|
||||||
|
continue
|
||||||
|
array = frame.to_ndarray(format="rgb24")
|
||||||
|
if array.shape != (height, width, 3):
|
||||||
|
raise M48QCaseMiningError("decoded raw-fisheye raster changed")
|
||||||
|
name = f"frame-{sequence:06d}.jpg"
|
||||||
|
path = cases_root / name
|
||||||
|
Image.fromarray(array, mode="RGB").save(
|
||||||
|
path,
|
||||||
|
format="JPEG",
|
||||||
|
quality=94,
|
||||||
|
subsampling=0,
|
||||||
|
optimize=False,
|
||||||
|
)
|
||||||
|
rendered[sequence] = {
|
||||||
|
"path": f"cases/{name}",
|
||||||
|
"media_type": "image/jpeg",
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": sha256_path(path),
|
||||||
|
"geometric_resampling": False,
|
||||||
|
}
|
||||||
|
if len(rendered) == len(wanted):
|
||||||
|
break
|
||||||
|
except (FFmpegError, OSError) as exc:
|
||||||
|
raise M48QCaseMiningError("RAVNOVES00 raw-fisheye frames cannot be decoded") from exc
|
||||||
|
if set(rendered) != wanted:
|
||||||
|
raise M48QCaseMiningError("not every selected raw-fisheye frame was decoded")
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def _case_document(
|
||||||
|
candidate: FrameCandidate,
|
||||||
|
*,
|
||||||
|
image: Mapping[str, object],
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"schema_version": CASE_SCHEMA,
|
||||||
|
"case_id": f"{candidate.sequence:06d}",
|
||||||
|
"sequence": candidate.sequence,
|
||||||
|
"frame_id": candidate.frame_id,
|
||||||
|
"evidence_time_ns": candidate.evidence_time_ns,
|
||||||
|
"selection_buckets": sorted(candidate.buckets),
|
||||||
|
"comparison": {
|
||||||
|
"native_detection_count": candidate.native_count,
|
||||||
|
"legacy_704_detection_count": candidate.legacy_count,
|
||||||
|
"matched_detection_count_iou_at_least_0_5": candidate.matched_count,
|
||||||
|
"quality_interpretation": "diagnostic-only",
|
||||||
|
},
|
||||||
|
"image": dict(image),
|
||||||
|
"proposals": [
|
||||||
|
{
|
||||||
|
"proposal_id": item.proposal_id,
|
||||||
|
"class_name": item.class_name,
|
||||||
|
"risk_family": item.risk_family,
|
||||||
|
"score": round(item.score, 9),
|
||||||
|
"box_xyxy": [round(value, 6) for value in item.box_xyxy],
|
||||||
|
}
|
||||||
|
for item in candidate.proposals
|
||||||
|
],
|
||||||
|
"ground_truth": False,
|
||||||
|
"quality_evaluated": False,
|
||||||
|
"authority": dict(FALSE_AUTHORITY),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run(args: argparse.Namespace) -> dict[str, object]:
|
||||||
|
for path in (
|
||||||
|
args.profile,
|
||||||
|
args.graph_result,
|
||||||
|
args.graph_frames,
|
||||||
|
args.comparison_result,
|
||||||
|
args.comparison_frames,
|
||||||
|
args.video,
|
||||||
|
):
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
raise M48QCaseMiningError(f"required evidence is missing: {path.name}")
|
||||||
|
profile_sha256 = _verify_hash(args.profile, args.expected_profile_sha256, "profile")
|
||||||
|
graph_result_sha256 = _verify_hash(
|
||||||
|
args.graph_result, args.expected_graph_result_sha256, "graph result"
|
||||||
|
)
|
||||||
|
graph_frames_sha256 = _verify_hash(
|
||||||
|
args.graph_frames, args.expected_graph_frames_sha256, "graph frames"
|
||||||
|
)
|
||||||
|
comparison_result_sha256 = _verify_hash(
|
||||||
|
args.comparison_result,
|
||||||
|
args.expected_comparison_result_sha256,
|
||||||
|
"comparison result",
|
||||||
|
)
|
||||||
|
comparison_frames_sha256 = _verify_hash(
|
||||||
|
args.comparison_frames,
|
||||||
|
args.expected_comparison_frames_sha256,
|
||||||
|
"comparison frames",
|
||||||
|
)
|
||||||
|
video_sha256 = _verify_hash(args.video, args.expected_video_sha256, "video")
|
||||||
|
runner_sha256 = _verify_hash(Path(__file__), args.expected_runner_sha256, "runner")
|
||||||
|
|
||||||
|
profile = _read_object(args.profile)
|
||||||
|
graph_result = _read_object(args.graph_result)
|
||||||
|
comparison_result = _read_object(args.comparison_result)
|
||||||
|
source = profile.get("source")
|
||||||
|
candidate = profile.get("candidate")
|
||||||
|
selection = profile.get("selection")
|
||||||
|
scope = profile.get("scope")
|
||||||
|
if (
|
||||||
|
profile.get("schema_version") != PROFILE_SCHEMA
|
||||||
|
or not isinstance(source, dict)
|
||||||
|
or source.get("video_sha256") != video_sha256
|
||||||
|
or source.get("raster_width") != 800
|
||||||
|
or source.get("raster_height") != 600
|
||||||
|
or source.get("frame_count") != 4489
|
||||||
|
or source.get("geometric_resampling") is not False
|
||||||
|
or source.get("rectification") is not False
|
||||||
|
or source.get("warp") is not False
|
||||||
|
or not isinstance(candidate, dict)
|
||||||
|
or candidate.get("engine_sha256")
|
||||||
|
!= "b8a40b3580edff001ec9680de68707242294ff590ab296000fae371f1083f695"
|
||||||
|
or not isinstance(selection, dict)
|
||||||
|
or selection.get("case_count") != 24
|
||||||
|
or not isinstance(selection.get("bucket_quotas"), dict)
|
||||||
|
or sum(selection["bucket_quotas"].values()) != 24
|
||||||
|
or scope
|
||||||
|
!= {
|
||||||
|
"quality_evaluated": False,
|
||||||
|
"ground_truth": False,
|
||||||
|
"candidate_accepted": False,
|
||||||
|
"production_accepted": False,
|
||||||
|
"purpose": "bounded-native-risk-case-review",
|
||||||
|
}
|
||||||
|
or profile.get("authority") != FALSE_AUTHORITY
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("M4.8Q profile contract changed")
|
||||||
|
if (
|
||||||
|
graph_result.get("schema_version") != "missioncore.m48s-reference-graph-shadow-load/v5"
|
||||||
|
or graph_result.get("completed") is not True
|
||||||
|
or graph_result.get("identity", {}).get("detector_provider_id") != candidate["provider_id"]
|
||||||
|
or graph_result.get("identity", {}).get("inputs", {}).get("video") != video_sha256
|
||||||
|
or graph_result.get("execution", {}).get("frame_evidence", {}).get("sha256")
|
||||||
|
!= graph_frames_sha256
|
||||||
|
or graph_result.get("authority") != FALSE_AUTHORITY
|
||||||
|
or graph_result.get("production_accepted") is not False
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("native graph result contract changed")
|
||||||
|
if (
|
||||||
|
comparison_result.get("schema_version") != "missioncore.m48n-native-vs-704-ravnoves00/v0"
|
||||||
|
or comparison_result.get("completed") is not True
|
||||||
|
or comparison_result.get("source", {}).get("video_sha256") != video_sha256
|
||||||
|
or comparison_result.get("source", {}).get("frame_count") != source["frame_count"]
|
||||||
|
or comparison_result.get("execution", {}).get("frames_sha256") != comparison_frames_sha256
|
||||||
|
or comparison_result.get("authority") != FALSE_AUTHORITY
|
||||||
|
):
|
||||||
|
raise M48QCaseMiningError("native/legacy comparison result contract changed")
|
||||||
|
|
||||||
|
output_root = args.output_root
|
||||||
|
if output_root.exists():
|
||||||
|
raise M48QCaseMiningError("M4.8Q output root already exists")
|
||||||
|
output_root.mkdir(mode=0o700, parents=True)
|
||||||
|
comparisons = load_comparison_rows(args.comparison_frames, source["frame_count"])
|
||||||
|
candidates = load_graph_candidates(
|
||||||
|
args.graph_frames,
|
||||||
|
profile=profile,
|
||||||
|
comparisons=comparisons,
|
||||||
|
)
|
||||||
|
selected = select_cases(
|
||||||
|
candidates,
|
||||||
|
bucket_quotas=selection["bucket_quotas"],
|
||||||
|
minimum_sequence_separation=selection["minimum_sequence_separation"],
|
||||||
|
)
|
||||||
|
rendered = render_selected_frames(
|
||||||
|
video_path=args.video,
|
||||||
|
selected=selected,
|
||||||
|
cases_root=output_root / "cases",
|
||||||
|
width=source["raster_width"],
|
||||||
|
height=source["raster_height"],
|
||||||
|
)
|
||||||
|
cases = [
|
||||||
|
_case_document(
|
||||||
|
item,
|
||||||
|
image=rendered[item.sequence],
|
||||||
|
)
|
||||||
|
for item in selected
|
||||||
|
]
|
||||||
|
cases_path = output_root / "cases.jsonl"
|
||||||
|
with cases_path.open("xb") as stream:
|
||||||
|
for case in cases:
|
||||||
|
stream.write(canonical_json(case) + b"\n")
|
||||||
|
|
||||||
|
class_counts = Counter(proposal.class_name for item in selected for proposal in item.proposals)
|
||||||
|
selected_bucket_coverage = Counter(bucket for item in selected for bucket in item.buckets)
|
||||||
|
identity = {
|
||||||
|
"schema_version": RESULT_SCHEMA,
|
||||||
|
"profile": {"profile_id": profile["profile_id"], "sha256": profile_sha256},
|
||||||
|
"source": {
|
||||||
|
"source_id": source["source_id"],
|
||||||
|
"video_sha256": video_sha256,
|
||||||
|
"graph_result_sha256": graph_result_sha256,
|
||||||
|
"graph_frames_sha256": graph_frames_sha256,
|
||||||
|
"comparison_result_sha256": comparison_result_sha256,
|
||||||
|
"comparison_frames_sha256": comparison_frames_sha256,
|
||||||
|
},
|
||||||
|
"candidate": dict(candidate),
|
||||||
|
"runner_sha256": runner_sha256,
|
||||||
|
"cases_sha256": sha256_path(cases_path),
|
||||||
|
"authority": dict(FALSE_AUTHORITY),
|
||||||
|
}
|
||||||
|
result: dict[str, object] = {
|
||||||
|
"schema_version": RESULT_SCHEMA,
|
||||||
|
"status": "complete-review-ready-quality-not-adjudicated",
|
||||||
|
"completed": True,
|
||||||
|
"worker": {"worker_id": "worker-006", "node": "DESKTOP-OPJ8J04"},
|
||||||
|
"identity": identity,
|
||||||
|
"report_identity_sha256": hashlib.sha256(canonical_json(identity)).hexdigest(),
|
||||||
|
"source": {
|
||||||
|
**dict(source),
|
||||||
|
"graph_result_sha256": graph_result_sha256,
|
||||||
|
"graph_frames_sha256": graph_frames_sha256,
|
||||||
|
"comparison_result_sha256": comparison_result_sha256,
|
||||||
|
"comparison_frames_sha256": comparison_frames_sha256,
|
||||||
|
},
|
||||||
|
"candidate": dict(candidate),
|
||||||
|
"selection": {
|
||||||
|
"case_count": len(cases),
|
||||||
|
"minimum_sequence_separation": selection["minimum_sequence_separation"],
|
||||||
|
"configured_bucket_quotas": selection["bucket_quotas"],
|
||||||
|
"selected_bucket_coverage": dict(sorted(selected_bucket_coverage.items())),
|
||||||
|
"selected_sequences": [item.sequence for item in selected],
|
||||||
|
"selected_class_counts": dict(sorted(class_counts.items())),
|
||||||
|
},
|
||||||
|
"artifacts": {
|
||||||
|
"cases": {
|
||||||
|
"path": "cases.jsonl",
|
||||||
|
"schema_version": CASE_SCHEMA,
|
||||||
|
"row_count": len(cases),
|
||||||
|
"sha256": sha256_path(cases_path),
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"root": "cases",
|
||||||
|
"count": len(rendered),
|
||||||
|
"geometric_resampling": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"quality_evaluated": False,
|
||||||
|
"ground_truth": False,
|
||||||
|
"candidate_accepted": False,
|
||||||
|
"production_accepted": False,
|
||||||
|
"next_action": "operator-adjudication-in-existing-m48-review-instrument",
|
||||||
|
},
|
||||||
|
"limitations": [
|
||||||
|
"The selected RAVNOVES00 cases are diagnostic samples, not independent truth.",
|
||||||
|
(
|
||||||
|
"Native-versus-legacy detection-count differences are not quality "
|
||||||
|
"verdicts because legacy 704 stretches the raw 4:3 raster."
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"No child/adult, behavior, physical track identity, navigation or "
|
||||||
|
"actuation claim is made."
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"JPEG review derivatives preserve the 800x600 raster without geometric "
|
||||||
|
"resampling but are not lossless source frames."
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"authority": dict(FALSE_AUTHORITY),
|
||||||
|
}
|
||||||
|
(output_root / "result.json").write_bytes(canonical_json(result) + b"\n")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
for name in (
|
||||||
|
"profile",
|
||||||
|
"graph-result",
|
||||||
|
"graph-frames",
|
||||||
|
"comparison-result",
|
||||||
|
"comparison-frames",
|
||||||
|
"video",
|
||||||
|
"output-root",
|
||||||
|
):
|
||||||
|
parser.add_argument(f"--{name}", type=Path, required=True)
|
||||||
|
for name in (
|
||||||
|
"profile",
|
||||||
|
"graph-result",
|
||||||
|
"graph-frames",
|
||||||
|
"comparison-result",
|
||||||
|
"comparison-frames",
|
||||||
|
"video",
|
||||||
|
"runner",
|
||||||
|
):
|
||||||
|
parser.add_argument(f"--expected-{name}-sha256", required=True)
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
try:
|
||||||
|
result = run(parse_args(argv))
|
||||||
|
except M48QCaseMiningError as exc:
|
||||||
|
print(f"M4.8Q case mining refused: {exc}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Publish the immutable M4.8Q native raw-fisheye review LAB result."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
build_m48q_native_risk_quality_lab,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--repository-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
result = build_m48q_native_risk_quality_lab(
|
||||||
|
repository_root=arguments.repository_root,
|
||||||
|
output_root=arguments.output_root,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"result_root": str(result.result_root),
|
||||||
|
"status": result.manifest["status"],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ReleaseRoot,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
|
||||||
|
[string]$RunId,
|
||||||
|
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\results\m48q-native-risk-case-mining"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ProgressPreference = "SilentlyContinue"
|
||||||
|
|
||||||
|
function Assert-LastExitCode([string]$Operation) {
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Sha256([string]$Path) {
|
||||||
|
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-DDirectory([string]$Path, [string]$Label, [bool]$Create) {
|
||||||
|
if ($Create -and -not (Test-Path -LiteralPath $Path)) {
|
||||||
|
$null = New-Item -ItemType Directory -Path $Path
|
||||||
|
}
|
||||||
|
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
|
||||||
|
if (
|
||||||
|
-not $item.PSIsContainer -or
|
||||||
|
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
|
||||||
|
[IO.Path]::GetPathRoot($item.FullName).TrimEnd("\") -ine "D:"
|
||||||
|
) {
|
||||||
|
throw "$Label must be a real D: directory"
|
||||||
|
}
|
||||||
|
return $item.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-RegularFile([string]$Path, [string]$Label) {
|
||||||
|
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
|
||||||
|
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
|
||||||
|
throw "$Label must be a regular file"
|
||||||
|
}
|
||||||
|
return $item.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-ToDockerPath([string]$Path) { return $Path.Replace("\", "/") }
|
||||||
|
|
||||||
|
function Get-Container([string]$Name) {
|
||||||
|
$rows = @((& docker inspect $Name) | ConvertFrom-Json)
|
||||||
|
Assert-LastExitCode "Docker inspection for $Name"
|
||||||
|
if ($rows.Count -ne 1) { throw "Container identity for $Name is not unique" }
|
||||||
|
return $rows[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($env:COMPUTERNAME -cne "DESKTOP-OPJ8J04") {
|
||||||
|
throw "M4.8Q native risk case mining is pinned to DESKTOP-OPJ8J04"
|
||||||
|
}
|
||||||
|
|
||||||
|
$release = Resolve-DDirectory $ReleaseRoot "M4.8Q release root" $false
|
||||||
|
$output = Resolve-DDirectory $OutputRoot "M4.8Q output root" $true
|
||||||
|
$runOutput = Join-Path $output $RunId
|
||||||
|
if (Test-Path -LiteralPath $runOutput) { throw "M4.8Q output already exists" }
|
||||||
|
|
||||||
|
$runner = Assert-RegularFile (
|
||||||
|
Join-Path $release "run_m48q_native_risk_case_mining_worker.py"
|
||||||
|
) "M4.8Q runner"
|
||||||
|
$profile = Assert-RegularFile (
|
||||||
|
Join-Path $release "m48q-native-risk-case-mining-v1.json"
|
||||||
|
) "M4.8Q profile"
|
||||||
|
$runnerSha256 = Get-Sha256 $runner
|
||||||
|
$profileSha256 = Get-Sha256 $profile
|
||||||
|
if ($runnerSha256 -cne "39bb1d810b167d6e97dcb091505b52d4a812d69419dcb32fe23ea5d14d605d67") {
|
||||||
|
throw "M4.8Q runner SHA-256 changed"
|
||||||
|
}
|
||||||
|
if ($profileSha256 -cne "c455055e63578d505edc80b66d67e795916a3d0297cc18bfee5a6af7b032ceda") {
|
||||||
|
throw "M4.8Q profile SHA-256 changed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$graphRoot = Resolve-DDirectory (
|
||||||
|
"D:\NDC_MISSIONCORE\runtime\results\m48n-native-reference-graph-shadow\ravnoves00-full-12fps-v0"
|
||||||
|
) "M4.8Q graph evidence root" $false
|
||||||
|
$comparisonRoot = Resolve-DDirectory (
|
||||||
|
"D:\NDC_MISSIONCORE\runtime\results\m48n-native-vs-704\ravnoves00-full-v0"
|
||||||
|
) "M4.8Q comparison evidence root" $false
|
||||||
|
$graphResult = Assert-RegularFile (Join-Path $graphRoot "result.json") "graph result"
|
||||||
|
$graphFrames = Assert-RegularFile (Join-Path $graphRoot "frames.jsonl") "graph frames"
|
||||||
|
$comparisonResult = Assert-RegularFile (Join-Path $comparisonRoot "result.json") "comparison result"
|
||||||
|
$comparisonFrames = Assert-RegularFile (Join-Path $comparisonRoot "frames.jsonl") "comparison frames"
|
||||||
|
$video = Assert-RegularFile (
|
||||||
|
"D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4"
|
||||||
|
) "RAVNOVES00 video"
|
||||||
|
|
||||||
|
$graphResultSha256 = Get-Sha256 $graphResult
|
||||||
|
$graphFramesSha256 = Get-Sha256 $graphFrames
|
||||||
|
$comparisonResultSha256 = Get-Sha256 $comparisonResult
|
||||||
|
$comparisonFramesSha256 = Get-Sha256 $comparisonFrames
|
||||||
|
$videoSha256 = Get-Sha256 $video
|
||||||
|
if ($graphResultSha256 -cne "c5c3a831b1d1c3271161c91fa5c5c40533eb663ca288ed0220334a147aed6e15") {
|
||||||
|
throw "M4.8Q graph result SHA-256 changed"
|
||||||
|
}
|
||||||
|
if ($graphFramesSha256 -cne "b245af969600670d0975e89cae02b44206e3f1328eff48d5b4639b1b2ab57346") {
|
||||||
|
throw "M4.8Q graph frame evidence SHA-256 changed"
|
||||||
|
}
|
||||||
|
if ($comparisonResultSha256 -cne "fd91c2f1f477038d5af51656d510aba39d69ad2e1a387f15d98ade19dd3a0c49") {
|
||||||
|
throw "M4.8Q comparison result SHA-256 changed"
|
||||||
|
}
|
||||||
|
if ($comparisonFramesSha256 -cne "6a35125d4e003edfd8a33b4239bad0910f137fde3a770e9d5baab721cdc145b2") {
|
||||||
|
throw "M4.8Q comparison frame evidence SHA-256 changed"
|
||||||
|
}
|
||||||
|
if ($videoSha256 -cne "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8") {
|
||||||
|
throw "RAVNOVES00 video SHA-256 changed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$media = Resolve-DDirectory (
|
||||||
|
"D:\NDC_MISSIONCORE\runtime\derived\perception-e15-media-pyav180-lz445-v1"
|
||||||
|
) "PyAV dependency" $false
|
||||||
|
$pillow = Resolve-DDirectory (
|
||||||
|
"D:\NDC_MISSIONCORE\runtime\derived\perception-p0-env-v1"
|
||||||
|
) "Pillow dependency" $false
|
||||||
|
$image = (
|
||||||
|
"nvcr.io/nvidia/tritonserver:26.06-py3@" +
|
||||||
|
"sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794"
|
||||||
|
)
|
||||||
|
& docker image inspect $image *> $null
|
||||||
|
Assert-LastExitCode "pinned M4.8Q image inspection"
|
||||||
|
|
||||||
|
$canonicalTriton = Get-Container "ndc-mission-core-triton"
|
||||||
|
if (-not $canonicalTriton.State.Running -or $canonicalTriton.State.Health.Status -cne "healthy") {
|
||||||
|
throw "Canonical Triton must remain healthy during M4.8Q mining"
|
||||||
|
}
|
||||||
|
$canonicalTritonId = [string]$canonicalTriton.Id
|
||||||
|
$runnerName = "ndc-mission-core-m48q-native-risk-case-mining"
|
||||||
|
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$runnerName$") {
|
||||||
|
throw "M4.8Q bounded container already exists"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
& docker run `
|
||||||
|
--name $runnerName `
|
||||||
|
--label "com.nodedc.product=mission-core" `
|
||||||
|
--label "com.nodedc.stack=ndc-mission-core-compute" `
|
||||||
|
--label "com.nodedc.role=bounded-native-risk-case-mining" `
|
||||||
|
--label "com.nodedc.managed-by=codex-bounded-experiment" `
|
||||||
|
--network none `
|
||||||
|
--read-only `
|
||||||
|
--security-opt "no-new-privileges:true" `
|
||||||
|
--cap-drop ALL `
|
||||||
|
--pids-limit 128 `
|
||||||
|
--cpus 4 `
|
||||||
|
--memory 4g `
|
||||||
|
--tmpfs "/tmp:rw,noexec,nosuid,size=512m" `
|
||||||
|
-e "PYTHONDONTWRITEBYTECODE=1" `
|
||||||
|
-e "PYTHONPATH=/opt/media:/opt/pillow" `
|
||||||
|
-v ((Convert-ToDockerPath $release) + ":/release:ro") `
|
||||||
|
-v ((Convert-ToDockerPath $output) + ":/output:rw") `
|
||||||
|
-v ((Convert-ToDockerPath $media) + ":/opt/media:ro") `
|
||||||
|
-v ((Convert-ToDockerPath $pillow) + ":/opt/pillow:ro") `
|
||||||
|
-v ((Convert-ToDockerPath $graphRoot) + ":/evidence/graph:ro") `
|
||||||
|
-v ((Convert-ToDockerPath $comparisonRoot) + ":/evidence/comparison:ro") `
|
||||||
|
-v ((Convert-ToDockerPath $video) + ":/source/right.mp4:ro") `
|
||||||
|
--entrypoint python3 `
|
||||||
|
$image `
|
||||||
|
/release/run_m48q_native_risk_case_mining_worker.py `
|
||||||
|
--profile /release/m48q-native-risk-case-mining-v1.json `
|
||||||
|
--graph-result /evidence/graph/result.json `
|
||||||
|
--graph-frames /evidence/graph/frames.jsonl `
|
||||||
|
--comparison-result /evidence/comparison/result.json `
|
||||||
|
--comparison-frames /evidence/comparison/frames.jsonl `
|
||||||
|
--video /source/right.mp4 `
|
||||||
|
--output-root ("/output/{0}" -f $RunId) `
|
||||||
|
--expected-profile-sha256 $profileSha256 `
|
||||||
|
--expected-graph-result-sha256 $graphResultSha256 `
|
||||||
|
--expected-graph-frames-sha256 $graphFramesSha256 `
|
||||||
|
--expected-comparison-result-sha256 $comparisonResultSha256 `
|
||||||
|
--expected-comparison-frames-sha256 $comparisonFramesSha256 `
|
||||||
|
--expected-video-sha256 $videoSha256 `
|
||||||
|
--expected-runner-sha256 $runnerSha256
|
||||||
|
Assert-LastExitCode "M4.8Q native risk case mining"
|
||||||
|
if (
|
||||||
|
-not (Test-Path -LiteralPath (Join-Path $runOutput "result.json") -PathType Leaf) -or
|
||||||
|
-not (Test-Path -LiteralPath (Join-Path $runOutput "cases.jsonl") -PathType Leaf) -or
|
||||||
|
@(Get-ChildItem -LiteralPath (Join-Path $runOutput "cases") -Filter "*.jpg" -File).Count -ne 24
|
||||||
|
) {
|
||||||
|
throw "M4.8Q result artifacts are incomplete"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$runnerName$") {
|
||||||
|
& docker rm -f $runnerName *> $null
|
||||||
|
}
|
||||||
|
$canonicalAfter = Get-Container "ndc-mission-core-triton"
|
||||||
|
if (
|
||||||
|
[string]$canonicalAfter.Id -cne $canonicalTritonId -or
|
||||||
|
-not $canonicalAfter.State.Running -or
|
||||||
|
$canonicalAfter.State.Health.Status -cne "healthy"
|
||||||
|
) {
|
||||||
|
throw "Canonical Triton changed during M4.8Q mining"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Get-Content -LiteralPath (Join-Path $runOutput "result.json") -Raw | ConvertFrom-Json
|
||||||
|
[pscustomobject]@{
|
||||||
|
run_id = $RunId
|
||||||
|
output_root = $runOutput
|
||||||
|
status = $result.status
|
||||||
|
case_count = $result.selection.case_count
|
||||||
|
report_identity_sha256 = $result.report_identity_sha256
|
||||||
|
canonical_triton = "healthy-and-unchanged"
|
||||||
|
} | ConvertTo-Json -Depth 4
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Publish one canonical append-only M4.8R2 static-occupancy qualification."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from k1link.compute.pipeline_telemetry import JsonlPipelineTelemetrySink
|
||||||
|
from k1link.laboratory import (
|
||||||
|
LaboratoryEvidenceRegistry,
|
||||||
|
LaboratoryExecutionRegistry,
|
||||||
|
LaboratoryRunner,
|
||||||
|
LaboratoryRunRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--profile", type=Path, required=True)
|
||||||
|
parser.add_argument("--m47-lab-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--graph-result-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--small-static-result-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--receipt-root", type=Path, required=True)
|
||||||
|
parser.add_argument("--telemetry-path", type=Path, required=True)
|
||||||
|
parser.add_argument("--run-id", required=True)
|
||||||
|
parser.add_argument("--request-id", required=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = _parser().parse_args()
|
||||||
|
repository_root = Path(__file__).resolve().parents[1]
|
||||||
|
evidence = LaboratoryEvidenceRegistry.from_directory(
|
||||||
|
repository_root / "config" / "laboratories"
|
||||||
|
)
|
||||||
|
execution = LaboratoryExecutionRegistry.from_file(
|
||||||
|
repository_root / "config" / "laboratory-execution.json",
|
||||||
|
evidence,
|
||||||
|
)
|
||||||
|
runner = LaboratoryRunner(
|
||||||
|
registry=execution,
|
||||||
|
evidence_registry=evidence,
|
||||||
|
sink=JsonlPipelineTelemetrySink(args.telemetry_path),
|
||||||
|
)
|
||||||
|
result = runner.run(
|
||||||
|
LaboratoryRunRequest(
|
||||||
|
work_id="m48-static-occupancy-qualification",
|
||||||
|
run_id=args.run_id,
|
||||||
|
request_id=args.request_id,
|
||||||
|
contour_id="mission-core-laboratory",
|
||||||
|
agent_id="local-control-plane",
|
||||||
|
node_id=socket.gethostname(),
|
||||||
|
source_id="RAVNOVES00",
|
||||||
|
source_package_id=args.m47_lab_root.name,
|
||||||
|
method_id="m48-conservative-static-occupancy/v1",
|
||||||
|
inputs={
|
||||||
|
"repository_root": repository_root,
|
||||||
|
"profile_path": args.profile,
|
||||||
|
"m47_lab_root": args.m47_lab_root,
|
||||||
|
"graph_result_root": args.graph_result_root,
|
||||||
|
"small_static_result_root": args.small_static_result_root,
|
||||||
|
},
|
||||||
|
output_root=args.output_root,
|
||||||
|
receipt_root=args.receipt_root,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(json.dumps({
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"result_root": str(result.result_root),
|
||||||
|
"receipt_id": result.receipt_id,
|
||||||
|
"receipt_root": str(result.receipt_root),
|
||||||
|
}, ensure_ascii=False, sort_keys=True))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -312,6 +312,9 @@ class LaboratoryRunner:
|
|||||||
|
|
||||||
def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
|
def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
|
||||||
return {
|
return {
|
||||||
|
"canonical.m48-static-occupancy-qualification/v1": (
|
||||||
|
_run_m48_static_occupancy_qualification
|
||||||
|
),
|
||||||
"canonical.m48-small-static-passage-regression/v1": (
|
"canonical.m48-small-static-passage-regression/v1": (
|
||||||
_run_m48_small_static_passage_regression
|
_run_m48_small_static_passage_regression
|
||||||
),
|
),
|
||||||
@@ -326,6 +329,27 @@ def canonical_laboratory_adapters() -> dict[str, LaboratoryAdapter]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _run_m48_static_occupancy_qualification(
|
||||||
|
request: LaboratoryRunRequest,
|
||||||
|
) -> LaboratoryAdapterResult:
|
||||||
|
from k1link.laboratory.m48_static_occupancy_qualification import (
|
||||||
|
build_m48_static_occupancy_qualification,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = build_m48_static_occupancy_qualification(
|
||||||
|
repository_root=request.inputs["repository_root"],
|
||||||
|
profile_path=request.inputs["profile_path"],
|
||||||
|
m47_lab_root=request.inputs["m47_lab_root"],
|
||||||
|
graph_result_root=request.inputs["graph_result_root"],
|
||||||
|
small_static_result_root=request.inputs["small_static_result_root"],
|
||||||
|
output_root=request.output_root,
|
||||||
|
)
|
||||||
|
return LaboratoryAdapterResult(
|
||||||
|
result_root=result.result_root,
|
||||||
|
result_id=result.result_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _run_m48s_fixed_class_detector(
|
def _run_m48s_fixed_class_detector(
|
||||||
request: LaboratoryRunRequest,
|
request: LaboratoryRunRequest,
|
||||||
) -> LaboratoryAdapterResult:
|
) -> LaboratoryAdapterResult:
|
||||||
|
|||||||
@@ -0,0 +1,754 @@
|
|||||||
|
"""Immutable M4.8R2 qualification of conservative static LiDAR occupancy.
|
||||||
|
|
||||||
|
The experiment does not run a detector and does not mutate the accepted M4.7
|
||||||
|
graph. It measures the accepted current/rolling occupied output on the frozen
|
||||||
|
operator-assisted small-static anchors, then evaluates one additive CPU-only
|
||||||
|
candidate already present in the local-surface artifact: low step candidates.
|
||||||
|
Neither missing evidence nor a camera miss is ever converted to free space.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import uuid
|
||||||
|
from dataclasses import dataclass, replace
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from k1link.laboratory.m47_reference_graph import (
|
||||||
|
M47ReferenceGraphLabError,
|
||||||
|
read_m47_reference_graph_lab,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48_small_static_regression import (
|
||||||
|
M48SmallStaticRegressionError,
|
||||||
|
read_m48_small_static_passage_regression,
|
||||||
|
)
|
||||||
|
from k1link.perception.geometry import RecordedGeometryStore
|
||||||
|
from k1link.perception.geometry_math import (
|
||||||
|
POINT_OCCUPIED,
|
||||||
|
project_map_points_kb4,
|
||||||
|
semantic_geometry_support,
|
||||||
|
)
|
||||||
|
|
||||||
|
M48_STATIC_OCCUPANCY_PROFILE_SCHEMA: Final = (
|
||||||
|
"missioncore.m48-static-occupancy-qualification-profile/v1"
|
||||||
|
)
|
||||||
|
M48_STATIC_OCCUPANCY_RESULT_SCHEMA: Final = (
|
||||||
|
"missioncore.m48-static-occupancy-qualification-result/v1"
|
||||||
|
)
|
||||||
|
M48_STATIC_OCCUPANCY_REPORT_SCHEMA: Final = (
|
||||||
|
"missioncore.m48-static-occupancy-qualification-report/v1"
|
||||||
|
)
|
||||||
|
M48_STATIC_OCCUPANCY_CASE_SCHEMA: Final = "missioncore.m48-static-occupancy-case/v1"
|
||||||
|
M48_STATIC_OCCUPANCY_CANONICAL_SCHEMA: Final = (
|
||||||
|
"missioncore.m48-static-occupancy-canonical-anchor/v1"
|
||||||
|
)
|
||||||
|
M48_STATIC_OCCUPANCY_PREFIX: Final = "m48-static-occupancy-qualification-"
|
||||||
|
|
||||||
|
_METHOD_SCHEMA: Final = "missioncore.laboratory-method/v1"
|
||||||
|
_AUTHORITY: Final = {
|
||||||
|
"mode": "replay-simulated",
|
||||||
|
"physical_live": False,
|
||||||
|
"commands_enabled": False,
|
||||||
|
"actuation_allowed": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class M48StaticOccupancyQualificationError(RuntimeError):
|
||||||
|
"""The static-occupancy source, method, or immutable result is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class M48StaticOccupancyQualificationResult:
|
||||||
|
result_id: str
|
||||||
|
result_root: Path
|
||||||
|
manifest: dict[str, Any]
|
||||||
|
report: dict[str, Any]
|
||||||
|
cases: tuple[dict[str, Any], ...]
|
||||||
|
canonical_anchors: tuple[dict[str, Any], ...]
|
||||||
|
|
||||||
|
|
||||||
|
def build_m48_static_occupancy_qualification(
|
||||||
|
*,
|
||||||
|
repository_root: Path,
|
||||||
|
profile_path: Path,
|
||||||
|
m47_lab_root: Path,
|
||||||
|
graph_result_root: Path,
|
||||||
|
small_static_result_root: Path,
|
||||||
|
output_root: Path,
|
||||||
|
run_created_at_utc: str | None = None,
|
||||||
|
) -> M48StaticOccupancyQualificationResult:
|
||||||
|
"""Publish one deterministic, append-only M4.8R2 qualification result."""
|
||||||
|
|
||||||
|
repository = repository_root.resolve(strict=True)
|
||||||
|
profile_bytes, profile = _read_profile(profile_path)
|
||||||
|
source = _object(profile["source"], "M4.8R2 source")
|
||||||
|
try:
|
||||||
|
m47 = read_m47_reference_graph_lab(m47_lab_root)
|
||||||
|
small_static = read_m48_small_static_passage_regression(small_static_result_root)
|
||||||
|
except (M47ReferenceGraphLabError, M48SmallStaticRegressionError) as exc:
|
||||||
|
raise M48StaticOccupancyQualificationError(
|
||||||
|
"accepted M4.7/M4.8R1 evidence is invalid"
|
||||||
|
) from exc
|
||||||
|
if (
|
||||||
|
m47.result_id != source.get("m47_lab_result_id")
|
||||||
|
or small_static.result_id != source.get("small_static_result_id")
|
||||||
|
or m47.manifest.get("accepted") is not True
|
||||||
|
or m47.manifest.get("ground_truth") is not False
|
||||||
|
):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 source identity changed")
|
||||||
|
|
||||||
|
anchors_path = small_static.result_root / "anchors.jsonl"
|
||||||
|
if _file_sha256(anchors_path) != source.get("small_static_anchors_sha256"):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 anchor ledger changed")
|
||||||
|
|
||||||
|
graph_root = graph_result_root.resolve(strict=True)
|
||||||
|
graph_manifest = _read_json(graph_root / "manifest.json", maximum=2 * 1024 * 1024)
|
||||||
|
frames_path = graph_root / "frames.jsonl"
|
||||||
|
graph_files = _object(graph_manifest.get("files"), "M4.8R2 graph files")
|
||||||
|
graph_frames = _object(graph_files.get("frames.jsonl"), "M4.8R2 graph frame artifact")
|
||||||
|
if (
|
||||||
|
graph_root.name != source.get("m47_graph_result_id")
|
||||||
|
or graph_manifest.get("result_id") != graph_root.name
|
||||||
|
or graph_manifest.get("accepted") is not True
|
||||||
|
or graph_frames.get("sha256") != source.get("m47_graph_frames_sha256")
|
||||||
|
or _file_sha256(frames_path) != source.get("m47_graph_frames_sha256")
|
||||||
|
):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 graph binding changed")
|
||||||
|
|
||||||
|
selection = _object(profile["selection"], "M4.8R2 selection")
|
||||||
|
anchors = tuple(
|
||||||
|
row
|
||||||
|
for row in small_static.anchors
|
||||||
|
if row.get("motion") == selection.get("motion")
|
||||||
|
and row.get("requires_avoidance_or_clearance")
|
||||||
|
is selection.get("requires_avoidance_or_clearance")
|
||||||
|
)
|
||||||
|
if not anchors:
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 selected no static anchors")
|
||||||
|
frame_rows = _selected_graph_frames(frames_path, {int(row["sequence"]) for row in anchors})
|
||||||
|
store = RecordedGeometryStore.from_repository(repository)
|
||||||
|
candidate = _object(profile["candidate"], "M4.8R2 candidate")
|
||||||
|
candidate_association = replace(
|
||||||
|
store.profile.association,
|
||||||
|
semantic_minimum_occupied_points=int(candidate["minimum_points"]),
|
||||||
|
semantic_minimum_occupied_voxels=int(candidate["minimum_voxels"]),
|
||||||
|
semantic_voxel_size_m=float(candidate["voxel_size_m"]),
|
||||||
|
depth_cluster_minimum_gap_m=float(candidate["depth_cluster_minimum_gap_m"]),
|
||||||
|
depth_cluster_gap_fraction=float(candidate["depth_cluster_gap_fraction"]),
|
||||||
|
spatial_cluster_radius_m=float(candidate["spatial_cluster_radius_m"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
cases: list[dict[str, Any]] = []
|
||||||
|
for anchor in anchors:
|
||||||
|
sequence = int(anchor["sequence"])
|
||||||
|
graph_row = frame_rows[sequence]
|
||||||
|
frame = store.frame_for_index(sequence)
|
||||||
|
if frame is None or not frame.surface_valid:
|
||||||
|
raise M48StaticOccupancyQualificationError(
|
||||||
|
"selected M4.8R2 anchor lacks qualified current LiDAR"
|
||||||
|
)
|
||||||
|
projected = project_map_points_kb4(
|
||||||
|
frame.points_map,
|
||||||
|
position_map_xyz=frame.sensor_position_map,
|
||||||
|
orientation_map_from_lidar_xyzw=frame.sensor_orientation_xyzw,
|
||||||
|
profile=frame.projection,
|
||||||
|
)
|
||||||
|
bbox = _pixel_bbox(anchor["extent_xyxy"], frame.projection.width, frame.projection.height)
|
||||||
|
baseline = semantic_geometry_support(
|
||||||
|
bbox,
|
||||||
|
projected=projected,
|
||||||
|
frame_points_map=frame.points_map,
|
||||||
|
point_class=frame.point_class,
|
||||||
|
profile=store.profile.association,
|
||||||
|
)
|
||||||
|
step_candidates = store.point_step_candidates_for_frame(sequence)
|
||||||
|
if step_candidates is None:
|
||||||
|
raise M48StaticOccupancyQualificationError(
|
||||||
|
"selected M4.8R2 anchor lacks low-step evidence"
|
||||||
|
)
|
||||||
|
union_classes = np.array(frame.point_class, copy=True)
|
||||||
|
union_classes[step_candidates > 0] = POINT_OCCUPIED
|
||||||
|
additive = semantic_geometry_support(
|
||||||
|
bbox,
|
||||||
|
projected=projected,
|
||||||
|
frame_points_map=frame.points_map,
|
||||||
|
point_class=union_classes,
|
||||||
|
profile=candidate_association,
|
||||||
|
)
|
||||||
|
graph_matches = _graph_component_matches(
|
||||||
|
graph_row=graph_row,
|
||||||
|
frame=frame,
|
||||||
|
bbox=bbox,
|
||||||
|
voxel_size_m=0.45,
|
||||||
|
)
|
||||||
|
raw_depths = _depths_in_bbox(projected.pixels_xy, projected.depths_m, bbox)
|
||||||
|
distance = _support_distance(
|
||||||
|
additive.occupied_depths_m, baseline.occupied_depths_m, raw_depths
|
||||||
|
)
|
||||||
|
band = _distance_band(distance, _object(profile["distance_bands_m"], "distance bands"))
|
||||||
|
accepted_graph = bool(graph_matches)
|
||||||
|
baseline_qualified = bool(baseline.qualified or accepted_graph)
|
||||||
|
candidate_qualified = bool(additive.qualified or accepted_graph)
|
||||||
|
false_free = graph_row["obstacle_map"].get("free_space_claimed") is True
|
||||||
|
cases.append(
|
||||||
|
{
|
||||||
|
"schema_version": M48_STATIC_OCCUPANCY_CASE_SCHEMA,
|
||||||
|
"anchor_id": anchor["anchor_id"],
|
||||||
|
"clip_id": anchor["clip_id"],
|
||||||
|
"sequence": sequence,
|
||||||
|
"extent_xyxy": anchor["extent_xyxy"],
|
||||||
|
"distance_m": distance,
|
||||||
|
"distance_band": band,
|
||||||
|
"accepted_graph": {
|
||||||
|
"matched": accepted_graph,
|
||||||
|
"component_count": len(graph_matches),
|
||||||
|
"components": graph_matches,
|
||||||
|
"free_space_claimed": false_free,
|
||||||
|
},
|
||||||
|
"current_local_surface": _support_projection(baseline),
|
||||||
|
"additive_step_candidate": _support_projection(additive),
|
||||||
|
"baseline_qualified": baseline_qualified,
|
||||||
|
"candidate_qualified": candidate_qualified,
|
||||||
|
"outcome": (
|
||||||
|
"candidate-qualified"
|
||||||
|
if candidate_qualified
|
||||||
|
else "unresolved-unknown-never-free"
|
||||||
|
),
|
||||||
|
"authority": "operator-assisted-development-anchor-not-truth",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cases.sort(key=lambda row: (int(row["sequence"]), str(row["anchor_id"])))
|
||||||
|
|
||||||
|
visual_report = _read_json(
|
||||||
|
m47.result_root / "visual-report.json",
|
||||||
|
maximum=2 * 1024 * 1024,
|
||||||
|
)
|
||||||
|
canonical = _canonical_anchors(visual_report, selection)
|
||||||
|
metrics = _metrics(cases, canonical)
|
||||||
|
acceptance = _object(profile["acceptance"], "M4.8R2 acceptance")
|
||||||
|
gates = {
|
||||||
|
"critical_near_candidate_recall": (
|
||||||
|
metrics["critical_near_candidate_recall"]
|
||||||
|
>= float(acceptance["minimum_critical_near_candidate_recall"])
|
||||||
|
),
|
||||||
|
"approach_candidate_recall": (
|
||||||
|
metrics["approach_candidate_recall"]
|
||||||
|
>= float(acceptance["minimum_approach_candidate_recall"])
|
||||||
|
),
|
||||||
|
"canonical_engineering_recall": (
|
||||||
|
metrics["canonical_engineering_recall"]
|
||||||
|
>= float(acceptance["minimum_canonical_engineering_recall"])
|
||||||
|
),
|
||||||
|
"zero_false_free": metrics["false_free_count"]
|
||||||
|
<= int(acceptance["maximum_false_free_count"]),
|
||||||
|
"independent_truth_available": False,
|
||||||
|
}
|
||||||
|
near_ready = bool(
|
||||||
|
gates["critical_near_candidate_recall"]
|
||||||
|
and gates["canonical_engineering_recall"]
|
||||||
|
and gates["zero_false_free"]
|
||||||
|
)
|
||||||
|
accepted = bool(near_ready and gates["approach_candidate_recall"])
|
||||||
|
created_at = _utc_timestamp(run_created_at_utc or datetime.now(UTC).isoformat())
|
||||||
|
profile_sha256 = hashlib.sha256(profile_bytes).hexdigest()
|
||||||
|
producer_sha256 = _file_sha256(Path(__file__).resolve())
|
||||||
|
identity = {
|
||||||
|
"schema_version": M48_STATIC_OCCUPANCY_RESULT_SCHEMA,
|
||||||
|
"human_lab_id": profile["human_lab_id"],
|
||||||
|
"run_label": profile["run_label"],
|
||||||
|
"run_created_at_utc": created_at,
|
||||||
|
"pipeline_id": profile["pipeline_id"],
|
||||||
|
"experiment_id": profile["experiment_id"],
|
||||||
|
"profile_id": profile["profile_id"],
|
||||||
|
"profile_sha256": profile_sha256,
|
||||||
|
"producer_sha256": producer_sha256,
|
||||||
|
"source": source,
|
||||||
|
"selection": {
|
||||||
|
"operator_static_anchor_count": len(cases),
|
||||||
|
"canonical_engineering_anchor_count": len(canonical),
|
||||||
|
},
|
||||||
|
"authority": dict(_AUTHORITY),
|
||||||
|
}
|
||||||
|
result_id = M48_STATIC_OCCUPANCY_PREFIX + _canonical_sha256(identity)
|
||||||
|
method = {
|
||||||
|
"schema_version": _METHOD_SCHEMA,
|
||||||
|
"completeness": "complete",
|
||||||
|
"execution_class": "deterministic",
|
||||||
|
"pipeline_id": profile["pipeline_id"],
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"kind": "source",
|
||||||
|
"name": "accepted M4.7 current/rolling obstacle graph",
|
||||||
|
"version": source["m47_graph_result_id"],
|
||||||
|
"role": "immutable baseline occupied/unknown and threat decisions",
|
||||||
|
"identity_sha256": source["m47_graph_frames_sha256"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "source",
|
||||||
|
"name": "M4.8R1 operator-assisted static anchors",
|
||||||
|
"version": source["small_static_result_id"],
|
||||||
|
"role": "candidate-visible diagnostic anchors; not independent truth",
|
||||||
|
"identity_sha256": source["small_static_anchors_sha256"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "algorithm",
|
||||||
|
"name": "additive low-step static occupancy candidate",
|
||||||
|
"version": profile["profile_id"],
|
||||||
|
"role": "CPU-only occupied-or-unknown evidence; never clearing",
|
||||||
|
"identity_sha256": producer_sha256,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
report = {
|
||||||
|
"schema_version": M48_STATIC_OCCUPANCY_REPORT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"source": source,
|
||||||
|
"configuration": {
|
||||||
|
key: profile[key]
|
||||||
|
for key in (
|
||||||
|
"profile_id",
|
||||||
|
"pipeline_id",
|
||||||
|
"experiment_id",
|
||||||
|
"human_lab_id",
|
||||||
|
"run_label",
|
||||||
|
"distance_bands_m",
|
||||||
|
"candidate",
|
||||||
|
"acceptance",
|
||||||
|
"policy",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"method": method,
|
||||||
|
"metrics": metrics,
|
||||||
|
"gates": gates,
|
||||||
|
"decision": {
|
||||||
|
"state": "accepted-bounded-static-occupancy-qualification"
|
||||||
|
if accepted
|
||||||
|
else "partial-static-occupancy-qualification",
|
||||||
|
"critical_near_candidate_ready_for_shadow": near_ready,
|
||||||
|
"production_accepted": False,
|
||||||
|
"summary": (
|
||||||
|
"Accepted graph covers "
|
||||||
|
f"{metrics['baseline_qualified_count']}/{len(cases)} static assisted "
|
||||||
|
"anchors; the additive step candidate covers "
|
||||||
|
f"{metrics['candidate_qualified_count']}/{len(cases)}."
|
||||||
|
),
|
||||||
|
"next_action": (
|
||||||
|
"Integrate the additive step evidence as an occupied-only Worker shadow, "
|
||||||
|
"then measure full replay FPS, occupancy growth and the unresolved 8-12 m "
|
||||||
|
"case."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"limitations": [
|
||||||
|
(
|
||||||
|
"Operator-assisted anchors are candidate-visible development evidence, "
|
||||||
|
"not independent truth."
|
||||||
|
),
|
||||||
|
"Projected camera rectangles do not define physical 3D colliders or chassis clearance.",
|
||||||
|
(
|
||||||
|
"The step candidate may add conservative false occupancy and therefore "
|
||||||
|
"requires a full replay load/volume shadow before cutover."
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"No ray clearing, planner-authoritative free space, physical navigation, "
|
||||||
|
"command, actuation or collision-safety authority is granted."
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"authority": dict(_AUTHORITY),
|
||||||
|
}
|
||||||
|
destination = output_root.resolve(strict=False) / result_id
|
||||||
|
_publish_result(destination, identity, created_at, accepted, report, tuple(cases), canonical)
|
||||||
|
return read_m48_static_occupancy_qualification(destination)
|
||||||
|
|
||||||
|
|
||||||
|
def read_m48_static_occupancy_qualification(
|
||||||
|
result_root: Path,
|
||||||
|
) -> M48StaticOccupancyQualificationResult:
|
||||||
|
if result_root.is_symlink():
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 result root is invalid")
|
||||||
|
root = result_root.resolve(strict=True)
|
||||||
|
if root.name.startswith(M48_STATIC_OCCUPANCY_PREFIX) is False:
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 result root is invalid")
|
||||||
|
manifest = _read_json(root / "manifest.json", maximum=2 * 1024 * 1024)
|
||||||
|
report = _read_json(root / "report.json", maximum=4 * 1024 * 1024)
|
||||||
|
cases = tuple(_read_jsonl(root / "cases.jsonl"))
|
||||||
|
canonical = tuple(_read_jsonl(root / "canonical-anchors.jsonl"))
|
||||||
|
if (
|
||||||
|
manifest.get("schema_version") != M48_STATIC_OCCUPANCY_RESULT_SCHEMA
|
||||||
|
or manifest.get("result_id") != root.name
|
||||||
|
or report.get("schema_version") != M48_STATIC_OCCUPANCY_REPORT_SCHEMA
|
||||||
|
or report.get("result_id") != root.name
|
||||||
|
or manifest.get("ground_truth") is not False
|
||||||
|
or manifest.get("authority") != _AUTHORITY
|
||||||
|
):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 result identity changed")
|
||||||
|
identity = _object(manifest.get("identity"), "M4.8R2 identity")
|
||||||
|
identity_sha256 = _canonical_sha256(identity)
|
||||||
|
if root.name != M48_STATIC_OCCUPANCY_PREFIX + identity_sha256:
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 result identity changed")
|
||||||
|
artifacts = manifest.get("artifacts")
|
||||||
|
if not isinstance(artifacts, list):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 artifact proof changed")
|
||||||
|
artifact_paths = [
|
||||||
|
str(_object(item, "M4.8R2 artifact").get("path")) for item in artifacts
|
||||||
|
]
|
||||||
|
if sorted(artifact_paths) != [
|
||||||
|
"canonical-anchors.jsonl",
|
||||||
|
"cases.jsonl",
|
||||||
|
"report.json",
|
||||||
|
]:
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 artifact proof changed")
|
||||||
|
for artifact in artifacts:
|
||||||
|
item = _object(artifact, "M4.8R2 artifact")
|
||||||
|
path = root / str(item.get("path"))
|
||||||
|
if path.parent != root or _file_sha256(path) != item.get("sha256"):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 artifact proof changed")
|
||||||
|
if manifest.get("identity_sha256") != identity_sha256:
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 identity digest changed")
|
||||||
|
if not cases or any(
|
||||||
|
row.get("schema_version") != M48_STATIC_OCCUPANCY_CASE_SCHEMA for row in cases
|
||||||
|
):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 case ledger changed")
|
||||||
|
if any(
|
||||||
|
row.get("schema_version") != M48_STATIC_OCCUPANCY_CANONICAL_SCHEMA
|
||||||
|
for row in canonical
|
||||||
|
):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 canonical ledger changed")
|
||||||
|
return M48StaticOccupancyQualificationResult(
|
||||||
|
root.name, root, manifest, report, cases, canonical
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _support_projection(value: Any) -> dict[str, object]:
|
||||||
|
depths = value.occupied_depths_m
|
||||||
|
return {
|
||||||
|
"qualified": bool(value.qualified),
|
||||||
|
"projected_point_count": int(value.projected_points_in_region),
|
||||||
|
"occupied_point_count": int(value.occupied_points_in_region),
|
||||||
|
"clustered_occupied_point_count": int(value.occupied_source_indices.size),
|
||||||
|
"nearest_depth_m": None if not depths.size else round(float(np.min(depths)), 6),
|
||||||
|
"median_depth_m": None if not depths.size else round(float(np.median(depths)), 6),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _graph_component_matches(
|
||||||
|
*,
|
||||||
|
graph_row: dict[str, Any],
|
||||||
|
frame: Any,
|
||||||
|
bbox: tuple[float, float, float, float],
|
||||||
|
voxel_size_m: float,
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
obstacle_map = _object(graph_row.get("obstacle_map"), "M4.8R2 obstacle map")
|
||||||
|
threats = {
|
||||||
|
str(row.get("component_id")): row
|
||||||
|
for row in graph_row.get("threats", [])
|
||||||
|
if isinstance(row, dict)
|
||||||
|
}
|
||||||
|
matches: list[dict[str, object]] = []
|
||||||
|
for obstacle in obstacle_map.get("occupied", []):
|
||||||
|
item = _object(obstacle, "M4.8R2 occupied component")
|
||||||
|
cells = item.get("cells")
|
||||||
|
if not isinstance(cells, list) or not cells:
|
||||||
|
continue
|
||||||
|
points = np.asarray(
|
||||||
|
[
|
||||||
|
[
|
||||||
|
(int(cell["x"]) + 0.5) * voxel_size_m,
|
||||||
|
(int(cell["y"]) + 0.5) * voxel_size_m,
|
||||||
|
(int(cell["z"]) + 0.5) * voxel_size_m,
|
||||||
|
]
|
||||||
|
for cell in cells
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
projected = project_map_points_kb4(
|
||||||
|
points,
|
||||||
|
position_map_xyz=frame.sensor_position_map,
|
||||||
|
orientation_map_from_lidar_xyzw=frame.sensor_orientation_xyzw,
|
||||||
|
profile=frame.projection,
|
||||||
|
)
|
||||||
|
depths = _depths_in_bbox(projected.pixels_xy, projected.depths_m, bbox)
|
||||||
|
if not depths.size:
|
||||||
|
continue
|
||||||
|
component_id = str(item.get("component_id"))
|
||||||
|
assessment = _object(threats.get(component_id), "M4.8R2 threat assessment")
|
||||||
|
matches.append(
|
||||||
|
{
|
||||||
|
"component_id": component_id,
|
||||||
|
"state": item.get("state"),
|
||||||
|
"decision": assessment.get("decision"),
|
||||||
|
"projected_cell_count": int(depths.size),
|
||||||
|
"nearest_depth_m": round(float(np.min(depths)), 6),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
matches.sort(key=lambda row: (float(row["nearest_depth_m"]), str(row["component_id"])))
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_anchors(
|
||||||
|
report: dict[str, Any], selection: dict[str, Any]
|
||||||
|
) -> tuple[dict[str, Any], ...]:
|
||||||
|
metrics = _object(report.get("metrics"), "M4.7 visual metrics")
|
||||||
|
visual = _object(metrics.get("visual_evidence"), "M4.7 visual evidence")
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for sequence in selection.get("canonical_engineering_sequences", []):
|
||||||
|
regression = _object(
|
||||||
|
visual.get(f"frame_{sequence}_regression"),
|
||||||
|
"canonical regression",
|
||||||
|
)
|
||||||
|
for anchor in regression.get("engineering_anchors", []):
|
||||||
|
item = _object(anchor, "canonical anchor")
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"schema_version": M48_STATIC_OCCUPANCY_CANONICAL_SCHEMA,
|
||||||
|
"sequence": int(sequence),
|
||||||
|
"anchor_id": item.get("anchor_id"),
|
||||||
|
"matched": item.get("matched") is True,
|
||||||
|
"decision": item.get("decision"),
|
||||||
|
"must_assert_threat": item.get("must_assert_threat") is True,
|
||||||
|
"authority": "camera-reviewed-engineering-anchor-not-truth",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return tuple(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _metrics(
|
||||||
|
cases: list[dict[str, Any]], canonical: tuple[dict[str, Any], ...]
|
||||||
|
) -> dict[str, object]:
|
||||||
|
def band_rows(name: str) -> list[dict[str, Any]]:
|
||||||
|
return [row for row in cases if row["distance_band"] == name]
|
||||||
|
|
||||||
|
def rate(rows: list[dict[str, Any]], key: str) -> float:
|
||||||
|
if not rows:
|
||||||
|
return 0.0
|
||||||
|
return sum(bool(row[key]) for row in rows) / len(rows)
|
||||||
|
|
||||||
|
near = band_rows("critical-near")
|
||||||
|
approach = band_rows("approach")
|
||||||
|
return {
|
||||||
|
"operator_static_anchor_count": len(cases),
|
||||||
|
"baseline_qualified_count": sum(bool(row["baseline_qualified"]) for row in cases),
|
||||||
|
"candidate_qualified_count": sum(bool(row["candidate_qualified"]) for row in cases),
|
||||||
|
"unresolved_unknown_count": sum(not bool(row["candidate_qualified"]) for row in cases),
|
||||||
|
"critical_near_anchor_count": len(near),
|
||||||
|
"critical_near_baseline_recall": rate(near, "baseline_qualified"),
|
||||||
|
"critical_near_candidate_recall": rate(near, "candidate_qualified"),
|
||||||
|
"approach_anchor_count": len(approach),
|
||||||
|
"approach_baseline_recall": rate(approach, "baseline_qualified"),
|
||||||
|
"approach_candidate_recall": rate(approach, "candidate_qualified"),
|
||||||
|
"canonical_engineering_anchor_count": len(canonical),
|
||||||
|
"canonical_engineering_recall": sum(bool(row["matched"]) for row in canonical)
|
||||||
|
/ len(canonical)
|
||||||
|
if canonical
|
||||||
|
else 0.0,
|
||||||
|
"false_free_count": sum(bool(row["accepted_graph"]["free_space_claimed"]) for row in cases),
|
||||||
|
"independent_truth": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_graph_frames(path: Path, sequences: set[int]) -> dict[int, dict[str, Any]]:
|
||||||
|
rows: dict[int, dict[str, Any]] = {}
|
||||||
|
with path.open("r", encoding="utf-8") as stream:
|
||||||
|
for line in stream:
|
||||||
|
row = _object(json.loads(line), "M4.8R2 graph frame")
|
||||||
|
sequence = row.get("sequence")
|
||||||
|
if isinstance(sequence, int) and sequence in sequences:
|
||||||
|
rows[sequence] = row
|
||||||
|
if len(rows) == len(sequences):
|
||||||
|
break
|
||||||
|
if set(rows) != sequences:
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 graph frames are incomplete")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _pixel_bbox(value: object, width: int, height: int) -> tuple[float, float, float, float]:
|
||||||
|
extent = value if isinstance(value, list) else None
|
||||||
|
if extent is None or len(extent) != 4:
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 anchor extent is invalid")
|
||||||
|
return (
|
||||||
|
float(extent[0]) * width,
|
||||||
|
float(extent[1]) * height,
|
||||||
|
float(extent[2]) * width,
|
||||||
|
float(extent[3]) * height,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _depths_in_bbox(
|
||||||
|
pixels: np.ndarray, depths: np.ndarray, bbox: tuple[float, float, float, float]
|
||||||
|
) -> np.ndarray:
|
||||||
|
if not pixels.size:
|
||||||
|
return np.empty(0, dtype=np.float64)
|
||||||
|
inside = (
|
||||||
|
(pixels[:, 0] >= bbox[0])
|
||||||
|
& (pixels[:, 0] <= bbox[2])
|
||||||
|
& (pixels[:, 1] >= bbox[1])
|
||||||
|
& (pixels[:, 1] <= bbox[3])
|
||||||
|
)
|
||||||
|
return depths[inside]
|
||||||
|
|
||||||
|
|
||||||
|
def _support_distance(*values: np.ndarray) -> float:
|
||||||
|
for value in values:
|
||||||
|
if value.size:
|
||||||
|
return round(float(np.median(value)), 6)
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 anchor has no projected LiDAR depth")
|
||||||
|
|
||||||
|
|
||||||
|
def _distance_band(distance: float, bands: dict[str, Any]) -> str:
|
||||||
|
for key, label in (("critical_near", "critical-near"), ("approach", "approach")):
|
||||||
|
bounds = bands.get(key)
|
||||||
|
if (
|
||||||
|
isinstance(bounds, list)
|
||||||
|
and len(bounds) == 2
|
||||||
|
and float(bounds[0]) <= distance < float(bounds[1])
|
||||||
|
):
|
||||||
|
return label
|
||||||
|
return "outside-qualified-bands"
|
||||||
|
|
||||||
|
|
||||||
|
def _read_profile(path: Path) -> tuple[bytes, dict[str, Any]]:
|
||||||
|
encoded = path.resolve(strict=True).read_bytes()
|
||||||
|
profile = _object(json.loads(encoded), "M4.8R2 profile")
|
||||||
|
if (
|
||||||
|
profile.get("schema_version") != M48_STATIC_OCCUPANCY_PROFILE_SCHEMA
|
||||||
|
or profile.get("authority") != _AUTHORITY
|
||||||
|
):
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 profile is invalid")
|
||||||
|
return encoded, profile
|
||||||
|
|
||||||
|
|
||||||
|
def _publish_result(
|
||||||
|
destination: Path,
|
||||||
|
identity: dict[str, Any],
|
||||||
|
created_at: str,
|
||||||
|
accepted: bool,
|
||||||
|
report: dict[str, Any],
|
||||||
|
cases: tuple[dict[str, Any], ...],
|
||||||
|
canonical: tuple[dict[str, Any], ...],
|
||||||
|
) -> None:
|
||||||
|
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
staging = destination.parent / f".{destination.name}.{uuid.uuid4().hex}.tmp"
|
||||||
|
staging.mkdir(mode=0o700)
|
||||||
|
try:
|
||||||
|
_write_json(staging / "report.json", report)
|
||||||
|
_write_jsonl(staging / "cases.jsonl", cases)
|
||||||
|
_write_jsonl(staging / "canonical-anchors.jsonl", canonical)
|
||||||
|
artifacts = [
|
||||||
|
_artifact(staging / name, role)
|
||||||
|
for name, role in (
|
||||||
|
("cases.jsonl", "operator-static-anchor-comparisons"),
|
||||||
|
("canonical-anchors.jsonl", "accepted-canonical-engineering-anchors"),
|
||||||
|
("report.json", "m48-static-occupancy-report"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
manifest = {
|
||||||
|
"schema_version": M48_STATIC_OCCUPANCY_RESULT_SCHEMA,
|
||||||
|
"result_id": destination.name,
|
||||||
|
"identity_sha256": _canonical_sha256(identity),
|
||||||
|
"identity": identity,
|
||||||
|
"created_at_utc": created_at,
|
||||||
|
"accepted": accepted,
|
||||||
|
"ground_truth": False,
|
||||||
|
"authority": dict(_AUTHORITY),
|
||||||
|
"artifacts": artifacts,
|
||||||
|
}
|
||||||
|
_write_json(staging / "manifest.json", manifest)
|
||||||
|
if destination.exists():
|
||||||
|
existing = {
|
||||||
|
path.name: _file_sha256(path) for path in destination.iterdir() if path.is_file()
|
||||||
|
}
|
||||||
|
proposed = {
|
||||||
|
path.name: _file_sha256(path) for path in staging.iterdir() if path.is_file()
|
||||||
|
}
|
||||||
|
if existing != proposed:
|
||||||
|
raise M48StaticOccupancyQualificationError("immutable M4.8R2 identity collided")
|
||||||
|
shutil.rmtree(staging)
|
||||||
|
return
|
||||||
|
os.replace(staging, destination)
|
||||||
|
except BaseException:
|
||||||
|
shutil.rmtree(staging, ignore_errors=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"path": path.name,
|
||||||
|
"role": role,
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": _file_sha256(path),
|
||||||
|
"media_type": "application/x-ndjson" if path.suffix == ".jsonl" else "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_json(path: Path, *, maximum: int) -> dict[str, Any]:
|
||||||
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > maximum:
|
||||||
|
raise M48StaticOccupancyQualificationError(f"{path.name} is unavailable")
|
||||||
|
return _object(json.loads(path.read_text("utf-8")), path.name)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||||
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > 8 * 1024 * 1024:
|
||||||
|
raise M48StaticOccupancyQualificationError(f"{path.name} is unavailable")
|
||||||
|
return [
|
||||||
|
_object(json.loads(line), path.name)
|
||||||
|
for line in path.read_text("utf-8").splitlines()
|
||||||
|
if line.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _write_json(path: Path, value: object) -> None:
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_jsonl(path: Path, rows: tuple[dict[str, Any], ...]) -> None:
|
||||||
|
path.write_text(
|
||||||
|
"".join(
|
||||||
|
json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
|
||||||
|
for row in rows
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _file_sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_sha256(value: object) -> str:
|
||||||
|
return hashlib.sha256(
|
||||||
|
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _object(value: object, label: str) -> dict[str, Any]:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M48StaticOccupancyQualificationError(f"{label} is invalid")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _utc_timestamp(value: str) -> str:
|
||||||
|
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
if parsed.tzinfo is None:
|
||||||
|
raise M48StaticOccupancyQualificationError("M4.8R2 creation time is invalid")
|
||||||
|
return parsed.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"M48StaticOccupancyQualificationError",
|
||||||
|
"M48StaticOccupancyQualificationResult",
|
||||||
|
"build_m48_static_occupancy_qualification",
|
||||||
|
"read_m48_static_occupancy_qualification",
|
||||||
|
]
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
"""Seal native raw-fisheye M4.8Q risk review evidence as a terminal LAB phase."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from k1link.perception.fixed_class_detector_tournament import (
|
||||||
|
canonical_json,
|
||||||
|
false_authority,
|
||||||
|
sha256_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
LAB_SCHEMA: Final = "missioncore.m48q-native-risk-quality-lab/v1"
|
||||||
|
REPORT_SCHEMA: Final = "missioncore.m48q-native-risk-quality-report/v1"
|
||||||
|
CATALOG_SCHEMA: Final = "missioncore.m48q-native-risk-review-catalog/v1"
|
||||||
|
CASE_SCHEMA: Final = "missioncore.m48q-native-risk-review-case/v1"
|
||||||
|
RESULT_PREFIX: Final = "m48q-native-risk-quality-lab-"
|
||||||
|
PROFILE_RELATIVE_PATH: Final = Path("config/perception/m48q-native-risk-case-mining-v1.json")
|
||||||
|
DETECTOR_PROFILE_RELATIVE_PATH: Final = Path(
|
||||||
|
"config/perception/rf-detr-large-native-kb4-risk-shadow-v0.json"
|
||||||
|
)
|
||||||
|
WORKER_RELATIVE_ROOT: Final = Path(
|
||||||
|
".runtime/worker-results/m48q-native-risk-case-mining/ravnoves00-native-raw-review-v3"
|
||||||
|
)
|
||||||
|
EXPECTED_PROFILE_SHA256: Final = "c455055e63578d505edc80b66d67e795916a3d0297cc18bfee5a6af7b032ceda"
|
||||||
|
EXPECTED_DETECTOR_PROFILE_SHA256: Final = (
|
||||||
|
"dbf4da5dbad6c3c22b1280b46ffcad81719bd183c81c263a4859847d829019b6"
|
||||||
|
)
|
||||||
|
EXPECTED_WORKER_RESULT_SHA256: Final = (
|
||||||
|
"a8f98d6d68abd6fd627caa05aaa3f048d69279a989b07fc2552b508af909a623"
|
||||||
|
)
|
||||||
|
EXPECTED_CASES_SHA256: Final = "9cea2a4a4558d761bc76b0d70474673ff07210473f53bd32cf178e8e0c8e179c"
|
||||||
|
EXPECTED_REPORT_IDENTITY_SHA256: Final = (
|
||||||
|
"b744775c0e08d18d728f04021cd2acf9b1ed6fcacf1520352ee04e06a6d5d324"
|
||||||
|
)
|
||||||
|
CASE_ID: Final = re.compile(r"^[0-9]{6}$")
|
||||||
|
|
||||||
|
|
||||||
|
class M48QNativeRiskQualityLabError(RuntimeError):
|
||||||
|
"""Raised when native M4.8Q evidence cannot be sealed honestly."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class M48QNativeRiskQualityLabResult:
|
||||||
|
result_root: Path
|
||||||
|
result_id: str
|
||||||
|
manifest: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def build_m48q_native_risk_quality_lab(
|
||||||
|
*,
|
||||||
|
repository_root: Path,
|
||||||
|
output_root: Path,
|
||||||
|
) -> M48QNativeRiskQualityLabResult:
|
||||||
|
repository = repository_root.expanduser().resolve(strict=True)
|
||||||
|
profile_path = repository / PROFILE_RELATIVE_PATH
|
||||||
|
detector_profile_path = repository / DETECTOR_PROFILE_RELATIVE_PATH
|
||||||
|
worker_root = repository / WORKER_RELATIVE_ROOT
|
||||||
|
worker_result_path = worker_root / "result.json"
|
||||||
|
cases_path = worker_root / "cases.jsonl"
|
||||||
|
images_root = worker_root / "cases"
|
||||||
|
for path in (
|
||||||
|
profile_path,
|
||||||
|
detector_profile_path,
|
||||||
|
worker_result_path,
|
||||||
|
cases_path,
|
||||||
|
):
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
raise M48QNativeRiskQualityLabError(f"required M4.8Q evidence is missing: {path.name}")
|
||||||
|
if images_root.is_symlink() or not images_root.is_dir():
|
||||||
|
raise M48QNativeRiskQualityLabError("M4.8Q image evidence is missing")
|
||||||
|
expected_hashes = {
|
||||||
|
profile_path: EXPECTED_PROFILE_SHA256,
|
||||||
|
detector_profile_path: EXPECTED_DETECTOR_PROFILE_SHA256,
|
||||||
|
worker_result_path: EXPECTED_WORKER_RESULT_SHA256,
|
||||||
|
cases_path: EXPECTED_CASES_SHA256,
|
||||||
|
}
|
||||||
|
if any(sha256_path(path) != expected for path, expected in expected_hashes.items()):
|
||||||
|
raise M48QNativeRiskQualityLabError("M4.8Q source evidence identity changed")
|
||||||
|
|
||||||
|
profile = _read_object(profile_path)
|
||||||
|
detector_profile = _read_object(detector_profile_path)
|
||||||
|
worker_result = _read_object(worker_result_path)
|
||||||
|
cases = _read_cases(cases_path)
|
||||||
|
_validate_inputs(
|
||||||
|
profile=profile,
|
||||||
|
detector_profile=detector_profile,
|
||||||
|
worker_result=worker_result,
|
||||||
|
cases=cases,
|
||||||
|
images_root=images_root,
|
||||||
|
)
|
||||||
|
method = _method(profile, detector_profile, worker_result)
|
||||||
|
identity = {
|
||||||
|
"schema_version": LAB_SCHEMA,
|
||||||
|
"profile": {
|
||||||
|
"profile_id": profile["profile_id"],
|
||||||
|
"sha256": EXPECTED_PROFILE_SHA256,
|
||||||
|
},
|
||||||
|
"detector_profile": {
|
||||||
|
"profile_id": detector_profile["profile_id"],
|
||||||
|
"sha256": EXPECTED_DETECTOR_PROFILE_SHA256,
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"source_id": "RAVNOVES00",
|
||||||
|
"video_sha256": profile["source"]["video_sha256"],
|
||||||
|
"graph_result_sha256": worker_result["source"]["graph_result_sha256"],
|
||||||
|
"graph_frames_sha256": worker_result["source"]["graph_frames_sha256"],
|
||||||
|
"comparison_result_sha256": worker_result["source"]["comparison_result_sha256"],
|
||||||
|
"comparison_frames_sha256": worker_result["source"]["comparison_frames_sha256"],
|
||||||
|
"worker_result_sha256": EXPECTED_WORKER_RESULT_SHA256,
|
||||||
|
"cases_sha256": EXPECTED_CASES_SHA256,
|
||||||
|
},
|
||||||
|
"method": method,
|
||||||
|
"authority": false_authority(),
|
||||||
|
}
|
||||||
|
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
|
||||||
|
result_id = RESULT_PREFIX + identity_sha256
|
||||||
|
root = output_root.expanduser().absolute()
|
||||||
|
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
destination = root / result_id
|
||||||
|
if destination.exists():
|
||||||
|
manifest = _read_object(destination / "manifest.json")
|
||||||
|
if (
|
||||||
|
manifest.get("schema_version") != LAB_SCHEMA
|
||||||
|
or manifest.get("identity_sha256") != identity_sha256
|
||||||
|
or manifest.get("result_id") != result_id
|
||||||
|
):
|
||||||
|
raise M48QNativeRiskQualityLabError("existing M4.8Q LAB identity conflicts")
|
||||||
|
return M48QNativeRiskQualityLabResult(destination, result_id, manifest)
|
||||||
|
|
||||||
|
created_at_utc = datetime.now(UTC).isoformat(timespec="microseconds").replace("+00:00", "Z")
|
||||||
|
temporary = Path(tempfile.mkdtemp(prefix=".m48q-native-risk-lab-", dir=root))
|
||||||
|
try:
|
||||||
|
(temporary / "cases").mkdir(mode=0o700)
|
||||||
|
shutil.copyfile(profile_path, temporary / "profile.json")
|
||||||
|
shutil.copyfile(detector_profile_path, temporary / "detector-profile.json")
|
||||||
|
shutil.copyfile(worker_result_path, temporary / "worker-result.json")
|
||||||
|
shutil.copyfile(cases_path, temporary / "cases.jsonl")
|
||||||
|
|
||||||
|
catalog_cases: list[dict[str, object]] = []
|
||||||
|
for case in cases:
|
||||||
|
image = case["image"]
|
||||||
|
source_image = worker_root / image["path"]
|
||||||
|
destination_image = temporary / image["path"]
|
||||||
|
shutil.copyfile(source_image, destination_image)
|
||||||
|
catalog_cases.append(
|
||||||
|
{
|
||||||
|
"case_id": case["case_id"],
|
||||||
|
"sequence": case["sequence"],
|
||||||
|
"frame_id": case["frame_id"],
|
||||||
|
"evidence_time_ns": case["evidence_time_ns"],
|
||||||
|
"path": image["path"],
|
||||||
|
"media_type": "image/jpeg",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"byte_length": destination_image.stat().st_size,
|
||||||
|
"sha256": sha256_path(destination_image),
|
||||||
|
"geometric_resampling": False,
|
||||||
|
"selection_buckets": case["selection_buckets"],
|
||||||
|
"comparison": case["comparison"],
|
||||||
|
"proposals": case["proposals"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
catalog = {
|
||||||
|
"schema_version": CATALOG_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"case_count": len(catalog_cases),
|
||||||
|
"source_raster": {"width": 800, "height": 600},
|
||||||
|
"overlay": {
|
||||||
|
"client_rendered": True,
|
||||||
|
"toggleable": True,
|
||||||
|
"box_coordinates": "source-pixel-xyxy",
|
||||||
|
"class_names": True,
|
||||||
|
"scores": True,
|
||||||
|
},
|
||||||
|
"ground_truth": False,
|
||||||
|
"cases": catalog_cases,
|
||||||
|
}
|
||||||
|
catalog_path = temporary / "catalog.json"
|
||||||
|
catalog_path.write_bytes(canonical_json(catalog) + b"\n")
|
||||||
|
|
||||||
|
graph_qualification = detector_profile["qualification"][
|
||||||
|
"full_ravnoves00_integrated_reference_graph"
|
||||||
|
]
|
||||||
|
decision = {
|
||||||
|
"review_ready": True,
|
||||||
|
"quality_evaluated": False,
|
||||||
|
"ground_truth": False,
|
||||||
|
"candidate_accepted": False,
|
||||||
|
"production_accepted": False,
|
||||||
|
"next_action": "operator-adjudication-in-existing-m48-review-instrument",
|
||||||
|
}
|
||||||
|
report = {
|
||||||
|
"schema_version": REPORT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"source": {
|
||||||
|
**profile["source"],
|
||||||
|
"graph_result_sha256": worker_result["source"]["graph_result_sha256"],
|
||||||
|
"graph_frames_sha256": worker_result["source"]["graph_frames_sha256"],
|
||||||
|
"independent_route_truth_available": False,
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"candidate": profile["candidate"],
|
||||||
|
"selection": profile["selection"],
|
||||||
|
},
|
||||||
|
"method": method,
|
||||||
|
"execution": {
|
||||||
|
"worker": worker_result["worker"],
|
||||||
|
"full_graph_frames": graph_qualification["frame_count"],
|
||||||
|
"requested_source_rate_hz": graph_qualification["requested_source_rate_hz"],
|
||||||
|
"effective_world_state_fps": graph_qualification["effective_world_state_fps"],
|
||||||
|
"world_state_completion_p95_ms": graph_qualification[
|
||||||
|
"world_state_completion_p95_ms"
|
||||||
|
],
|
||||||
|
"detector_total_p95_ms": graph_qualification["detector_total_p95_ms"],
|
||||||
|
"gpu_utilization_p95_percent": graph_qualification["gpu_utilization_p95_percent"],
|
||||||
|
"gpu_memory_used_maximum_mib": graph_qualification["gpu_memory_used_maximum_mib"],
|
||||||
|
"additional_inference_passes": 0,
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"selection": worker_result["selection"],
|
||||||
|
"runtime": {
|
||||||
|
"delivery_ratio": graph_qualification["delivery_ratio"],
|
||||||
|
"integrated_runtime_gate_passed": graph_qualification[
|
||||||
|
"integrated_runtime_gate_passed"
|
||||||
|
],
|
||||||
|
"operating_target_gate_passed": graph_qualification[
|
||||||
|
"operating_target_gate_passed"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"native_tensor_parity": detector_profile["qualification"][
|
||||||
|
"native_pytorch_tensorrt_parity"
|
||||||
|
],
|
||||||
|
"native_vs_legacy_704": detector_profile["qualification"][
|
||||||
|
"full_ravnoves00_native_vs_legacy_704"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"review_ready": True,
|
||||||
|
"integrated_runtime_gate_passed": True,
|
||||||
|
"independent_quality_evaluated": False,
|
||||||
|
"semantic_candidate_accepted": False,
|
||||||
|
},
|
||||||
|
"decision": decision,
|
||||||
|
"limitations": worker_result["limitations"],
|
||||||
|
"authority": false_authority(),
|
||||||
|
"visual_evidence": {
|
||||||
|
"kind": "native-raw-fisheye-risk-case-review",
|
||||||
|
"case_count": 24,
|
||||||
|
"source_raster": "800x600",
|
||||||
|
"geometric_resampling": False,
|
||||||
|
"boxes_baked_into_images": False,
|
||||||
|
"ground_truth": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
report_path = temporary / "report.json"
|
||||||
|
report_path.write_bytes(canonical_json(report) + b"\n")
|
||||||
|
artifacts = _artifact_manifest(temporary)
|
||||||
|
manifest = {
|
||||||
|
"schema_version": LAB_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"identity_sha256": identity_sha256,
|
||||||
|
"identity": identity,
|
||||||
|
"created_at_utc": created_at_utc,
|
||||||
|
"status": "complete-review-ready-quality-not-adjudicated",
|
||||||
|
"completed": True,
|
||||||
|
"bounded_question_accepted": True,
|
||||||
|
"ground_truth": False,
|
||||||
|
"method": method,
|
||||||
|
"metrics": report["metrics"],
|
||||||
|
"decision": decision,
|
||||||
|
"limitations": worker_result["limitations"],
|
||||||
|
"authority": false_authority(),
|
||||||
|
"artifacts": artifacts,
|
||||||
|
}
|
||||||
|
(temporary / "manifest.json").write_bytes(canonical_json(manifest) + b"\n")
|
||||||
|
temporary.replace(destination)
|
||||||
|
except BaseException:
|
||||||
|
shutil.rmtree(temporary, ignore_errors=True)
|
||||||
|
raise
|
||||||
|
return M48QNativeRiskQualityLabResult(destination, result_id, manifest)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_inputs(
|
||||||
|
*,
|
||||||
|
profile: dict[str, Any],
|
||||||
|
detector_profile: dict[str, Any],
|
||||||
|
worker_result: dict[str, Any],
|
||||||
|
cases: list[dict[str, Any]],
|
||||||
|
images_root: Path,
|
||||||
|
) -> None:
|
||||||
|
if (
|
||||||
|
profile.get("schema_version") != "missioncore.m48q-native-risk-case-mining-profile/v1"
|
||||||
|
or profile.get("source", {}).get("geometric_resampling") is not False
|
||||||
|
or profile.get("source", {}).get("raster_width") != 800
|
||||||
|
or profile.get("source", {}).get("raster_height") != 600
|
||||||
|
or profile.get("authority") != false_authority()
|
||||||
|
or detector_profile.get("schema_version")
|
||||||
|
!= "missioncore.rf-detr-native-risk-shadow-profile/v0"
|
||||||
|
or detector_profile.get("preprocessing", {}).get("resize") is not False
|
||||||
|
or detector_profile.get("preprocessing", {}).get("geometric_resampling") is not False
|
||||||
|
or detector_profile.get("status", {}).get("integrated_world_state_gate_passed") is not True
|
||||||
|
or worker_result.get("schema_version")
|
||||||
|
!= "missioncore.m48q-native-risk-case-mining-result/v1"
|
||||||
|
or worker_result.get("status") != "complete-review-ready-quality-not-adjudicated"
|
||||||
|
or worker_result.get("completed") is not True
|
||||||
|
or worker_result.get("report_identity_sha256") != EXPECTED_REPORT_IDENTITY_SHA256
|
||||||
|
or worker_result.get("artifacts", {}).get("cases", {}).get("sha256")
|
||||||
|
!= EXPECTED_CASES_SHA256
|
||||||
|
or worker_result.get("selection", {}).get("case_count") != 24
|
||||||
|
or worker_result.get("decision", {}).get("quality_evaluated") is not False
|
||||||
|
or worker_result.get("authority") != false_authority()
|
||||||
|
or len(cases) != 24
|
||||||
|
or len({case.get("case_id") for case in cases}) != 24
|
||||||
|
):
|
||||||
|
raise M48QNativeRiskQualityLabError("M4.8Q evidence contract changed")
|
||||||
|
for case in cases:
|
||||||
|
image = case.get("image")
|
||||||
|
proposals = case.get("proposals")
|
||||||
|
if (
|
||||||
|
case.get("schema_version") != CASE_SCHEMA
|
||||||
|
or not isinstance(case.get("case_id"), str)
|
||||||
|
or CASE_ID.fullmatch(case["case_id"]) is None
|
||||||
|
or case.get("case_id") != f"{case.get('sequence'):06d}"
|
||||||
|
or case.get("frame_id") != f"frame-{case.get('sequence'):06d}"
|
||||||
|
or case.get("ground_truth") is not False
|
||||||
|
or case.get("quality_evaluated") is not False
|
||||||
|
or case.get("authority") != false_authority()
|
||||||
|
or not isinstance(image, dict)
|
||||||
|
or image.get("path") != f"cases/frame-{case['case_id']}.jpg"
|
||||||
|
or image.get("width") != 800
|
||||||
|
or image.get("height") != 600
|
||||||
|
or image.get("geometric_resampling") is not False
|
||||||
|
or not isinstance(proposals, list)
|
||||||
|
or not proposals
|
||||||
|
):
|
||||||
|
raise M48QNativeRiskQualityLabError("M4.8Q review case contract changed")
|
||||||
|
image_path = images_root.parent / image["path"]
|
||||||
|
if (
|
||||||
|
image_path.is_symlink()
|
||||||
|
or not image_path.is_file()
|
||||||
|
or image_path.stat().st_size != image.get("byte_length")
|
||||||
|
or sha256_path(image_path) != image.get("sha256")
|
||||||
|
):
|
||||||
|
raise M48QNativeRiskQualityLabError("M4.8Q review image proof changed")
|
||||||
|
|
||||||
|
|
||||||
|
def _method(
|
||||||
|
profile: dict[str, Any],
|
||||||
|
detector_profile: dict[str, Any],
|
||||||
|
worker_result: dict[str, Any],
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"schema_version": "missioncore.laboratory-method/v1",
|
||||||
|
"completeness": "complete",
|
||||||
|
"execution_class": "hybrid",
|
||||||
|
"pipeline_id": "m48q-native-raw-fisheye-risk-case-review/v1",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"kind": "source",
|
||||||
|
"name": "RAVNOVES00 raw KB4 video",
|
||||||
|
"version": "800x600 immutable recording",
|
||||||
|
"role": "unrectified review raster",
|
||||||
|
"identity_sha256": profile["source"]["video_sha256"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "model",
|
||||||
|
"name": "RF-DETR-L native KB4 TensorRT",
|
||||||
|
"version": profile["candidate"]["model_id"],
|
||||||
|
"role": "fixed-class behavior-risk shadow proposals",
|
||||||
|
"identity_sha256": profile["candidate"]["engine_sha256"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "runtime",
|
||||||
|
"name": "M4.7 native reference graph",
|
||||||
|
"version": "full RAVNOVES00 at recorded 12 FPS",
|
||||||
|
"role": "proposal and realtime evidence source",
|
||||||
|
"identity_sha256": worker_result["source"]["graph_result_sha256"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "algorithm",
|
||||||
|
"name": "bounded diagnostic case miner",
|
||||||
|
"version": profile["profile_id"],
|
||||||
|
"role": "deterministic risk-family and edge-case sampling",
|
||||||
|
"identity_sha256": worker_result["identity"]["runner_sha256"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "tool",
|
||||||
|
"name": "M4.8 existing image-case review instrument",
|
||||||
|
"version": "client-rendered source-pixel overlay/v1",
|
||||||
|
"role": "operator review without baked boxes",
|
||||||
|
"identity_sha256": EXPECTED_CASES_SHA256,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact_manifest(root: Path) -> list[dict[str, object]]:
|
||||||
|
artifacts: list[dict[str, object]] = []
|
||||||
|
for path in sorted(item for item in root.rglob("*") if item.is_file()):
|
||||||
|
relative = path.relative_to(root).as_posix()
|
||||||
|
if relative == "manifest.json":
|
||||||
|
continue
|
||||||
|
media_type = "application/json"
|
||||||
|
schema_version: str | None = None
|
||||||
|
role = "supporting-evidence"
|
||||||
|
if relative == "report.json":
|
||||||
|
role = "laboratory-report"
|
||||||
|
schema_version = REPORT_SCHEMA
|
||||||
|
elif relative == "catalog.json":
|
||||||
|
role = "visual-evidence-catalog"
|
||||||
|
schema_version = CATALOG_SCHEMA
|
||||||
|
elif relative == "profile.json":
|
||||||
|
role = "predeclared-native-risk-review-profile"
|
||||||
|
schema_version = "missioncore.m48q-native-risk-case-mining-profile/v1"
|
||||||
|
elif relative == "detector-profile.json":
|
||||||
|
role = "qualified-native-detector-profile"
|
||||||
|
schema_version = "missioncore.rf-detr-native-risk-shadow-profile/v0"
|
||||||
|
elif relative == "worker-result.json":
|
||||||
|
role = "upstream-worker-case-mining-result"
|
||||||
|
schema_version = "missioncore.m48q-native-risk-case-mining-result/v1"
|
||||||
|
elif relative == "cases.jsonl":
|
||||||
|
media_type = "application/x-ndjson"
|
||||||
|
role = "native-risk-review-case-ledger"
|
||||||
|
schema_version = CASE_SCHEMA
|
||||||
|
elif relative.endswith(".jpg"):
|
||||||
|
media_type = "image/jpeg"
|
||||||
|
role = "visual-evidence-native-raw-fisheye-frame"
|
||||||
|
artifacts.append(
|
||||||
|
{
|
||||||
|
"role": role,
|
||||||
|
"path": relative,
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": sha256_path(path),
|
||||||
|
"media_type": media_type,
|
||||||
|
"schema_version": schema_version,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
|
||||||
|
def _read_object(path: Path) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
value: object = json.loads(path.read_text("utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise M48QNativeRiskQualityLabError(f"invalid JSON evidence: {path.name}") from exc
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M48QNativeRiskQualityLabError(f"JSON evidence must be an object: {path.name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _read_cases(path: Path) -> list[dict[str, Any]]:
|
||||||
|
cases: list[dict[str, Any]] = []
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8") as stream:
|
||||||
|
for line in stream:
|
||||||
|
value: object = json.loads(line)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise M48QNativeRiskQualityLabError("M4.8Q case ledger row is not an object")
|
||||||
|
cases.append(value)
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise M48QNativeRiskQualityLabError("M4.8Q case ledger cannot be read") from exc
|
||||||
|
return cases
|
||||||
@@ -344,6 +344,28 @@ class RecordedGeometryStore:
|
|||||||
points.setflags(write=False)
|
points.setflags(write=False)
|
||||||
return points
|
return points
|
||||||
|
|
||||||
|
def point_step_candidates_for_frame(self, frame_index: int) -> UInt8Array | None:
|
||||||
|
"""Expose the sealed low-step diagnostic in the source point index space.
|
||||||
|
|
||||||
|
The array is evidence only: a non-zero value may add conservative
|
||||||
|
occupied/unknown support, but it never clears a cell or claims free
|
||||||
|
space. Unavailable and surface-invalid frames remain unavailable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
frame = self.frame_for_index(frame_index)
|
||||||
|
if frame is None or not frame.surface_valid:
|
||||||
|
return None
|
||||||
|
offsets = self._source["cloud_offsets"]
|
||||||
|
start, end = int(offsets[frame_index]), int(offsets[frame_index + 1])
|
||||||
|
values = np.asarray(
|
||||||
|
self._surface["point_step_candidate"][start:end],
|
||||||
|
dtype=np.uint8,
|
||||||
|
)
|
||||||
|
if values.shape != (frame.source_point_count,):
|
||||||
|
raise GeometryProviderError("local-surface step evidence changed")
|
||||||
|
values.setflags(write=False)
|
||||||
|
return values
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def maximum_current_point_count(self) -> int:
|
def maximum_current_point_count(self) -> int:
|
||||||
"""Return the immutable source-pack upper bound for one recorded increment."""
|
"""Return the immutable source-pack upper bound for one recorded increment."""
|
||||||
|
|||||||
+15
-1
@@ -153,8 +153,8 @@ from k1link.web.runtime_readiness import (
|
|||||||
build_runtime_readiness,
|
build_runtime_readiness,
|
||||||
)
|
)
|
||||||
from k1link.web.session_api import build_session_router
|
from k1link.web.session_api import build_session_router
|
||||||
from k1link.web.simulation_world_provider_api import build_simulation_world_provider_router
|
|
||||||
from k1link.web.simulation_projects_api import build_simulation_projects_router
|
from k1link.web.simulation_projects_api import build_simulation_projects_router
|
||||||
|
from k1link.web.simulation_world_provider_api import build_simulation_world_provider_router
|
||||||
from k1link.web.system_telemetry_api import build_system_telemetry_router
|
from k1link.web.system_telemetry_api import build_system_telemetry_router
|
||||||
from k1link.web.viewer_diagnostics_api import build_viewer_diagnostics_router
|
from k1link.web.viewer_diagnostics_api import build_viewer_diagnostics_router
|
||||||
|
|
||||||
@@ -934,6 +934,13 @@ app.include_router(
|
|||||||
/ "m48"
|
/ "m48"
|
||||||
/ "small-static-passage-regression-results"
|
/ "small-static-passage-regression-results"
|
||||||
),
|
),
|
||||||
|
static_occupancy_result_root_provider=lambda: (
|
||||||
|
REPOSITORY_ROOT
|
||||||
|
/ ".runtime"
|
||||||
|
/ "compute-experiments"
|
||||||
|
/ "m48"
|
||||||
|
/ "static-occupancy-qualification-results"
|
||||||
|
),
|
||||||
camera_frame_provider=(
|
camera_frame_provider=(
|
||||||
session_recorded_camera_frame_service.extract
|
session_recorded_camera_frame_service.extract
|
||||||
if session_recorded_camera_frame_service is not None
|
if session_recorded_camera_frame_service is not None
|
||||||
@@ -980,6 +987,13 @@ app.include_router(
|
|||||||
/ "m48t-risk-quality"
|
/ "m48t-risk-quality"
|
||||||
/ "lab-results"
|
/ "lab-results"
|
||||||
),
|
),
|
||||||
|
native_root_provider=lambda: (
|
||||||
|
REPOSITORY_ROOT
|
||||||
|
/ ".runtime"
|
||||||
|
/ "compute-experiments"
|
||||||
|
/ "m48t-risk-quality"
|
||||||
|
/ "native-lab-results"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ from k1link.laboratory.m48_small_static_regression import (
|
|||||||
M48SmallStaticRegressionResult,
|
M48SmallStaticRegressionResult,
|
||||||
read_m48_small_static_passage_regression,
|
read_m48_small_static_passage_regression,
|
||||||
)
|
)
|
||||||
|
from k1link.laboratory.m48_static_occupancy_qualification import (
|
||||||
|
M48StaticOccupancyQualificationError,
|
||||||
|
M48StaticOccupancyQualificationResult,
|
||||||
|
read_m48_static_occupancy_qualification,
|
||||||
|
)
|
||||||
from k1link.sessions import RecordedCameraPlaybackSource
|
from k1link.sessions import RecordedCameraPlaybackSource
|
||||||
|
|
||||||
_PACK_ID = re.compile(r"^m48-object-quality-pack-[a-f0-9]{64}$")
|
_PACK_ID = re.compile(r"^m48-object-quality-pack-[a-f0-9]{64}$")
|
||||||
@@ -57,6 +62,9 @@ _SMALL_STATIC_RESULT_ID = re.compile(
|
|||||||
r"^m48-small-static-passage-regression-[a-f0-9]{64}$"
|
r"^m48-small-static-passage-regression-[a-f0-9]{64}$"
|
||||||
)
|
)
|
||||||
_SMALL_STATIC_ANCHOR_ID = re.compile(r"^anchor-[a-f0-9]{24}$")
|
_SMALL_STATIC_ANCHOR_ID = re.compile(r"^anchor-[a-f0-9]{24}$")
|
||||||
|
_STATIC_OCCUPANCY_RESULT_ID = re.compile(
|
||||||
|
r"^m48-static-occupancy-qualification-[a-f0-9]{64}$"
|
||||||
|
)
|
||||||
_M47_LAB_RESULT_ID = re.compile(r"^m47-reference-graph-lab-[a-f0-9]{64}$")
|
_M47_LAB_RESULT_ID = re.compile(r"^m47-reference-graph-lab-[a-f0-9]{64}$")
|
||||||
_FAILURE_ID = re.compile(r"^m48-failure-[a-f0-9]{64}$")
|
_FAILURE_ID = re.compile(r"^m48-failure-[a-f0-9]{64}$")
|
||||||
_REVIEW_SESSION_ID = re.compile(r"^m48-review-session-[a-f0-9]{64}$")
|
_REVIEW_SESSION_ID = re.compile(r"^m48-review-session-[a-f0-9]{64}$")
|
||||||
@@ -93,6 +101,12 @@ _SMALL_STATIC_CASE_CATALOG_SCHEMA: Final = (
|
|||||||
_SMALL_STATIC_CASE_VIEW_SCHEMA: Final = (
|
_SMALL_STATIC_CASE_VIEW_SCHEMA: Final = (
|
||||||
"missioncore.m48-small-static-passage-regression-case-view/v1"
|
"missioncore.m48-small-static-passage-regression-case-view/v1"
|
||||||
)
|
)
|
||||||
|
_STATIC_OCCUPANCY_RESULT_VIEW_SCHEMA: Final = (
|
||||||
|
"missioncore.m48-static-occupancy-qualification-result-view/v1"
|
||||||
|
)
|
||||||
|
_STATIC_OCCUPANCY_CASE_CATALOG_SCHEMA: Final = (
|
||||||
|
"missioncore.m48-static-occupancy-case-catalog/v1"
|
||||||
|
)
|
||||||
_FAILURE_ATLAS_VIEW_SCHEMA: Final = "missioncore.m48-object-quality-failure-atlas-view/v1"
|
_FAILURE_ATLAS_VIEW_SCHEMA: Final = "missioncore.m48-object-quality-failure-atlas-view/v1"
|
||||||
_FAILURE_CASE_VIEW_SCHEMA: Final = "missioncore.m48-object-quality-failure-case-view/v1"
|
_FAILURE_CASE_VIEW_SCHEMA: Final = "missioncore.m48-object-quality-failure-case-view/v1"
|
||||||
_REVIEW_CAPABILITY_HEADER: Final = "X-M48-Review-Capability"
|
_REVIEW_CAPABILITY_HEADER: Final = "X-M48-Review-Capability"
|
||||||
@@ -438,6 +452,7 @@ def build_m48_object_quality_router(
|
|||||||
truth_root_provider: RootProvider = lambda: None,
|
truth_root_provider: RootProvider = lambda: None,
|
||||||
result_root_provider: RootProvider = lambda: None,
|
result_root_provider: RootProvider = lambda: None,
|
||||||
small_static_result_root_provider: RootProvider = lambda: None,
|
small_static_result_root_provider: RootProvider = lambda: None,
|
||||||
|
static_occupancy_result_root_provider: RootProvider = lambda: None,
|
||||||
camera_frame_provider: CameraFrameProvider | None = None,
|
camera_frame_provider: CameraFrameProvider | None = None,
|
||||||
camera_playback_provider: CameraPlaybackProvider | None = None,
|
camera_playback_provider: CameraPlaybackProvider | None = None,
|
||||||
spatial_evidence_provider: SpatialEvidenceProvider | None = None,
|
spatial_evidence_provider: SpatialEvidenceProvider | None = None,
|
||||||
@@ -1593,6 +1608,56 @@ def build_m48_object_quality_router(
|
|||||||
"access": "assisted-development-regression-case-read-only",
|
"access": "assisted-development-regression-case-read-only",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@router.get("/regressions/static-occupancy/{result_id}")
|
||||||
|
def get_static_occupancy_qualification(
|
||||||
|
result_id: Annotated[str, ApiPath(pattern=_STATIC_OCCUPANCY_RESULT_ID.pattern)],
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = _resolve_static_occupancy_result(
|
||||||
|
static_occupancy_result_root_provider,
|
||||||
|
result_id,
|
||||||
|
)
|
||||||
|
source = _object(result.report.get("source"), "M4.8R2 source")
|
||||||
|
configuration = _object(
|
||||||
|
result.report.get("configuration"),
|
||||||
|
"M4.8R2 configuration",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"schema_version": _STATIC_OCCUPANCY_RESULT_VIEW_SCHEMA,
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"created_at_utc": result.manifest.get("created_at_utc"),
|
||||||
|
"reference_graph_lab_result_id": source.get("m47_lab_result_id"),
|
||||||
|
"small_static_result_id": source.get("small_static_result_id"),
|
||||||
|
"run_label": configuration.get("run_label"),
|
||||||
|
"pipeline_id": configuration.get("pipeline_id"),
|
||||||
|
"experiment_id": configuration.get("experiment_id"),
|
||||||
|
"accepted": result.manifest.get("accepted"),
|
||||||
|
"metrics": copy.deepcopy(result.report.get("metrics")),
|
||||||
|
"gates": copy.deepcopy(result.report.get("gates")),
|
||||||
|
"decision": copy.deepcopy(result.report.get("decision")),
|
||||||
|
"ground_truth": False,
|
||||||
|
"independent_truth": False,
|
||||||
|
"authority": dict(_AUTHORITY),
|
||||||
|
"access": "static-occupancy-qualification-read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/regressions/static-occupancy/{result_id}/cases")
|
||||||
|
def get_static_occupancy_qualification_cases(
|
||||||
|
result_id: Annotated[str, ApiPath(pattern=_STATIC_OCCUPANCY_RESULT_ID.pattern)],
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = _resolve_static_occupancy_result(
|
||||||
|
static_occupancy_result_root_provider,
|
||||||
|
result_id,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"schema_version": _STATIC_OCCUPANCY_CASE_CATALOG_SCHEMA,
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"cases": copy.deepcopy(result.cases),
|
||||||
|
"case_count": len(result.cases),
|
||||||
|
"ground_truth": False,
|
||||||
|
"authority": dict(_AUTHORITY),
|
||||||
|
"access": "static-occupancy-qualification-read-only",
|
||||||
|
}
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|
||||||
|
|
||||||
@@ -1691,6 +1756,27 @@ def _resolve_small_static_result(
|
|||||||
) from None
|
) from None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_static_occupancy_result(
|
||||||
|
provider: RootProvider,
|
||||||
|
result_id: str,
|
||||||
|
) -> M48StaticOccupancyQualificationResult:
|
||||||
|
if _STATIC_OCCUPANCY_RESULT_ID.fullmatch(result_id) is None:
|
||||||
|
raise HTTPException(status_code=404, detail="M4.8R2 result was not found")
|
||||||
|
root = _configured_root(provider)
|
||||||
|
if root is None:
|
||||||
|
raise HTTPException(status_code=404, detail="M4.8R2 result was not found")
|
||||||
|
path = root / result_id
|
||||||
|
if path.is_symlink() or not path.is_dir():
|
||||||
|
raise HTTPException(status_code=404, detail="M4.8R2 result was not found")
|
||||||
|
try:
|
||||||
|
resolved = path.resolve(strict=True)
|
||||||
|
if resolved.parent != root:
|
||||||
|
raise OSError("M4.8R2 result escaped root")
|
||||||
|
return read_m48_static_occupancy_qualification(resolved)
|
||||||
|
except (M48StaticOccupancyQualificationError, OSError, TypeError, ValueError):
|
||||||
|
raise HTTPException(status_code=404, detail="M4.8R2 result was not found") from None
|
||||||
|
|
||||||
|
|
||||||
def _result_sources(
|
def _result_sources(
|
||||||
result: M48ObjectQualityResult,
|
result: M48ObjectQualityResult,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Read-only API for the sealed M4.8T quality and temporal LAB."""
|
"""Read-only API for the M4.8T legacy quality and M4.8Q native review lifecycle."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from typing import Any, Final
|
from typing import Any, Final, Literal
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
@@ -18,30 +18,61 @@ from k1link.laboratory.evidence_report import (
|
|||||||
LaboratoryEvidenceReportError,
|
LaboratoryEvidenceReportError,
|
||||||
verify_laboratory_evidence_result,
|
verify_laboratory_evidence_result,
|
||||||
)
|
)
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
CATALOG_SCHEMA as NATIVE_CATALOG_SCHEMA,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
LAB_SCHEMA as NATIVE_LAB_SCHEMA,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
REPORT_SCHEMA as NATIVE_REPORT_SCHEMA,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
RESULT_PREFIX as NATIVE_RESULT_PREFIX,
|
||||||
|
)
|
||||||
from k1link.laboratory.m48t_risk_quality_lab import (
|
from k1link.laboratory.m48t_risk_quality_lab import (
|
||||||
CATALOG_SCHEMA,
|
CATALOG_SCHEMA as LEGACY_CATALOG_SCHEMA,
|
||||||
LAB_SCHEMA,
|
)
|
||||||
REPORT_SCHEMA,
|
from k1link.laboratory.m48t_risk_quality_lab import (
|
||||||
RESULT_PREFIX,
|
LAB_SCHEMA as LEGACY_LAB_SCHEMA,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48t_risk_quality_lab import (
|
||||||
|
REPORT_SCHEMA as LEGACY_REPORT_SCHEMA,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48t_risk_quality_lab import (
|
||||||
|
RESULT_PREFIX as LEGACY_RESULT_PREFIX,
|
||||||
)
|
)
|
||||||
from k1link.perception.fixed_class_detector_tournament import false_authority
|
from k1link.perception.fixed_class_detector_tournament import false_authority
|
||||||
|
|
||||||
RootProvider = Callable[[], Path | None]
|
RootProvider = Callable[[], Path | None]
|
||||||
RESULT_ID: Final = re.compile(rf"^{re.escape(RESULT_PREFIX)}[a-f0-9]{{64}}$")
|
Variant = Literal["legacy", "native"]
|
||||||
CASE_ID: Final = re.compile(r"^[0-9]{12}$")
|
LEGACY_RESULT_ID: Final = re.compile(rf"^{re.escape(LEGACY_RESULT_PREFIX)}[a-f0-9]{{64}}$")
|
||||||
VIEW_SCHEMA: Final = "missioncore.m48t-risk-quality-temporal-view/v1"
|
NATIVE_RESULT_ID: Final = re.compile(rf"^{re.escape(NATIVE_RESULT_PREFIX)}[a-f0-9]{{64}}$")
|
||||||
CATALOG_VIEW_SCHEMA: Final = "missioncore.m48t-risk-quality-temporal-catalog/v1"
|
LEGACY_CASE_ID: Final = re.compile(r"^[0-9]{12}$")
|
||||||
_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
NATIVE_CASE_ID: Final = re.compile(r"^[0-9]{6}$")
|
||||||
|
LEGACY_VIEW_SCHEMA: Final = "missioncore.m48t-risk-quality-temporal-view/v1"
|
||||||
|
NATIVE_VIEW_SCHEMA: Final = "missioncore.m48q-native-risk-quality-view/v1"
|
||||||
|
CATALOG_VIEW_SCHEMA: Final = "missioncore.m48t-risk-quality-lifecycle-catalog/v1"
|
||||||
|
_LEGACY_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||||
work_id="m48t-risk-quality-temporal",
|
work_id="m48t-risk-quality-temporal",
|
||||||
runtime_relative_root=PurePosixPath("m48t-risk-quality/lab-results"),
|
runtime_relative_root=PurePosixPath("m48t-risk-quality/lab-results"),
|
||||||
result_id_prefix="m48t-risk-quality-temporal-lab",
|
result_id_prefix="m48t-risk-quality-temporal-lab",
|
||||||
document_name="manifest.json",
|
document_name="manifest.json",
|
||||||
result_schema_version=LAB_SCHEMA,
|
result_schema_version=LEGACY_LAB_SCHEMA,
|
||||||
|
)
|
||||||
|
_NATIVE_DEFINITION: Final = LaboratoryEvidenceDefinition(
|
||||||
|
work_id="m48t-risk-quality-temporal",
|
||||||
|
runtime_relative_root=PurePosixPath("m48t-risk-quality/native-lab-results"),
|
||||||
|
result_id_prefix="m48q-native-risk-quality-lab",
|
||||||
|
document_name="manifest.json",
|
||||||
|
result_schema_version=NATIVE_LAB_SCHEMA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_m48t_risk_quality_lab_router(
|
def build_m48t_risk_quality_lab_router(
|
||||||
*, root_provider: RootProvider = lambda: None
|
*,
|
||||||
|
root_provider: RootProvider = lambda: None,
|
||||||
|
native_root_provider: RootProvider = lambda: None,
|
||||||
) -> APIRouter:
|
) -> APIRouter:
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
prefix="/api/v1/laboratory/m48t/risk-quality",
|
prefix="/api/v1/laboratory/m48t/risk-quality",
|
||||||
@@ -50,15 +81,23 @@ def build_m48t_risk_quality_lab_router(
|
|||||||
|
|
||||||
@router.get("/results")
|
@router.get("/results")
|
||||||
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
def list_results(limit: int = Query(default=1, ge=1, le=10)) -> dict[str, object]:
|
||||||
root = _configured_root(root_provider)
|
configured = False
|
||||||
|
candidates: list[tuple[Path, Variant]] = []
|
||||||
|
providers: tuple[tuple[RootProvider, Variant, re.Pattern[str]], ...] = (
|
||||||
|
(root_provider, "legacy", LEGACY_RESULT_ID),
|
||||||
|
(native_root_provider, "native", NATIVE_RESULT_ID),
|
||||||
|
)
|
||||||
|
for provider, variant, pattern in providers:
|
||||||
|
root = _configured_root(provider)
|
||||||
if root is None:
|
if root is None:
|
||||||
return _empty_catalog(False)
|
continue
|
||||||
candidates = _candidates(root)
|
configured = True
|
||||||
|
candidates.extend((item, variant) for item in _candidates(root, pattern))
|
||||||
items: list[dict[str, object]] = []
|
items: list[dict[str, object]] = []
|
||||||
invalid_total = 0
|
invalid_total = 0
|
||||||
for candidate in candidates:
|
for candidate, variant in candidates:
|
||||||
try:
|
try:
|
||||||
items.append(_project_result(candidate))
|
items.append(_project_result(candidate, variant))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
invalid_total += 1
|
invalid_total += 1
|
||||||
items.sort(
|
items.sort(
|
||||||
@@ -67,7 +106,7 @@ def build_m48t_risk_quality_lab_router(
|
|||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"schema_version": CATALOG_VIEW_SCHEMA,
|
"schema_version": CATALOG_VIEW_SCHEMA,
|
||||||
"configured": True,
|
"configured": configured,
|
||||||
"items": items[:limit],
|
"items": items[:limit],
|
||||||
"candidate_total": len(candidates),
|
"candidate_total": len(candidates),
|
||||||
"invalid_total": invalid_total,
|
"invalid_total": invalid_total,
|
||||||
@@ -77,18 +116,37 @@ def build_m48t_risk_quality_lab_router(
|
|||||||
@router.get("/results/{result_id}")
|
@router.get("/results/{result_id}")
|
||||||
def get_result(result_id: str) -> dict[str, object]:
|
def get_result(result_id: str) -> dict[str, object]:
|
||||||
try:
|
try:
|
||||||
return _project_result(_resolve_candidate(root_provider, result_id))
|
candidate, variant = _resolve_candidate(
|
||||||
|
root_provider,
|
||||||
|
native_root_provider,
|
||||||
|
result_id,
|
||||||
|
)
|
||||||
|
return _project_result(candidate, variant)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
raise HTTPException(status_code=404, detail="M4.8T result not found") from None
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="M4.8T/M4.8Q result not found",
|
||||||
|
) from None
|
||||||
|
|
||||||
@router.get("/results/{result_id}/review/{case_id}.jpg")
|
@router.get("/results/{result_id}/review/{case_id}.jpg")
|
||||||
def get_review_image(result_id: str, case_id: str) -> FileResponse:
|
def get_review_image(result_id: str, case_id: str) -> FileResponse:
|
||||||
if CASE_ID.fullmatch(case_id) is None:
|
|
||||||
raise HTTPException(status_code=404, detail="M4.8T review case not found")
|
|
||||||
try:
|
try:
|
||||||
loaded = _load_result(_resolve_candidate(root_provider, result_id))
|
candidate, variant = _resolve_candidate(
|
||||||
|
root_provider,
|
||||||
|
native_root_provider,
|
||||||
|
result_id,
|
||||||
|
)
|
||||||
|
invalid_case = (variant == "legacy" and LEGACY_CASE_ID.fullmatch(case_id) is None) or (
|
||||||
|
variant == "native" and NATIVE_CASE_ID.fullmatch(case_id) is None
|
||||||
|
)
|
||||||
|
if invalid_case:
|
||||||
|
raise RuntimeError("case identity is invalid")
|
||||||
|
loaded = _load_result(candidate, variant)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
raise HTTPException(status_code=404, detail="M4.8T result not found") from None
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="M4.8T/M4.8Q review case not found",
|
||||||
|
) from None
|
||||||
descriptor = next(
|
descriptor = next(
|
||||||
(
|
(
|
||||||
item
|
item
|
||||||
@@ -98,8 +156,10 @@ def build_m48t_risk_quality_lab_router(
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if not isinstance(descriptor, dict):
|
if not isinstance(descriptor, dict):
|
||||||
raise HTTPException(status_code=404, detail="M4.8T review case not found")
|
raise HTTPException(
|
||||||
candidate = loaded["root"]
|
status_code=404,
|
||||||
|
detail="M4.8T/M4.8Q review case not found",
|
||||||
|
)
|
||||||
path = (candidate / str(descriptor["path"])).resolve()
|
path = (candidate / str(descriptor["path"])).resolve()
|
||||||
if (
|
if (
|
||||||
not path.is_relative_to(candidate)
|
not path.is_relative_to(candidate)
|
||||||
@@ -108,7 +168,10 @@ def build_m48t_risk_quality_lab_router(
|
|||||||
or descriptor.get("byte_length") != path.stat().st_size
|
or descriptor.get("byte_length") != path.stat().st_size
|
||||||
or descriptor.get("sha256") != _sha256(path)
|
or descriptor.get("sha256") != _sha256(path)
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=404, detail="M4.8T review case not found")
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="M4.8T/M4.8Q review case not found",
|
||||||
|
)
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
path,
|
path,
|
||||||
media_type="image/jpeg",
|
media_type="image/jpeg",
|
||||||
@@ -122,8 +185,12 @@ def build_m48t_risk_quality_lab_router(
|
|||||||
return router
|
return router
|
||||||
|
|
||||||
|
|
||||||
def _project_result(candidate: Path) -> dict[str, object]:
|
def _project_result(candidate: Path, variant: Variant) -> dict[str, object]:
|
||||||
loaded = _load_result(candidate)
|
loaded = _load_result(candidate, variant)
|
||||||
|
return _project_native(loaded) if variant == "native" else _project_legacy(loaded)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_legacy(loaded: dict[str, Any]) -> dict[str, object]:
|
||||||
manifest = loaded["manifest"]
|
manifest = loaded["manifest"]
|
||||||
report = loaded["report"]
|
report = loaded["report"]
|
||||||
catalog = loaded["catalog"]
|
catalog = loaded["catalog"]
|
||||||
@@ -143,7 +210,8 @@ def _project_result(candidate: Path) -> dict[str, object]:
|
|||||||
for item in catalog["cases"]
|
for item in catalog["cases"]
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
"schema_version": VIEW_SCHEMA,
|
"schema_version": LEGACY_VIEW_SCHEMA,
|
||||||
|
"variant": "legacy-coco-quality",
|
||||||
"result_id": result_id,
|
"result_id": result_id,
|
||||||
"created_at_utc": manifest["created_at_utc"],
|
"created_at_utc": manifest["created_at_utc"],
|
||||||
"status": manifest["status"],
|
"status": manifest["status"],
|
||||||
@@ -165,26 +233,97 @@ def _project_result(candidate: Path) -> dict[str, object]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _load_result(candidate: Path) -> dict[str, Any]:
|
def _project_native(loaded: dict[str, Any]) -> dict[str, object]:
|
||||||
|
manifest = loaded["manifest"]
|
||||||
|
report = loaded["report"]
|
||||||
|
catalog = loaded["catalog"]
|
||||||
|
result_id = str(manifest["result_id"])
|
||||||
|
review_cases = [
|
||||||
|
{
|
||||||
|
"case_id": item["case_id"],
|
||||||
|
"sequence": item["sequence"],
|
||||||
|
"frame_id": item["frame_id"],
|
||||||
|
"evidence_time_ns": item["evidence_time_ns"],
|
||||||
|
"image_url": (
|
||||||
|
f"/api/v1/laboratory/m48t/risk-quality/results/{result_id}"
|
||||||
|
f"/review/{item['case_id']}.jpg"
|
||||||
|
),
|
||||||
|
"media_type": item["media_type"],
|
||||||
|
"width": item["width"],
|
||||||
|
"height": item["height"],
|
||||||
|
"byte_length": item["byte_length"],
|
||||||
|
"sha256": item["sha256"],
|
||||||
|
"geometric_resampling": item["geometric_resampling"],
|
||||||
|
"selection_buckets": copy.deepcopy(item["selection_buckets"]),
|
||||||
|
"comparison": copy.deepcopy(item["comparison"]),
|
||||||
|
"proposals": copy.deepcopy(item["proposals"]),
|
||||||
|
}
|
||||||
|
for item in catalog["cases"]
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"schema_version": NATIVE_VIEW_SCHEMA,
|
||||||
|
"variant": "native-risk-review",
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": manifest["created_at_utc"],
|
||||||
|
"status": manifest["status"],
|
||||||
|
"source": copy.deepcopy(report["source"]),
|
||||||
|
"configuration": copy.deepcopy(report["configuration"]),
|
||||||
|
"method": copy.deepcopy(report["method"]),
|
||||||
|
"execution": copy.deepcopy(report["execution"]),
|
||||||
|
"metrics": copy.deepcopy(report["metrics"]),
|
||||||
|
"acceptance": copy.deepcopy(report["acceptance"]),
|
||||||
|
"decision": copy.deepcopy(report["decision"]),
|
||||||
|
"limitations": copy.deepcopy(report["limitations"]),
|
||||||
|
"review": {
|
||||||
|
"source_raster": copy.deepcopy(catalog["source_raster"]),
|
||||||
|
"overlay": copy.deepcopy(catalog["overlay"]),
|
||||||
|
"cases": review_cases,
|
||||||
|
},
|
||||||
|
"ground_truth": False,
|
||||||
|
"authority": copy.deepcopy(report["authority"]),
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_result(candidate: Path, variant: Variant) -> dict[str, Any]:
|
||||||
|
pattern = NATIVE_RESULT_ID if variant == "native" else LEGACY_RESULT_ID
|
||||||
|
definition = _NATIVE_DEFINITION if variant == "native" else _LEGACY_DEFINITION
|
||||||
if (
|
if (
|
||||||
not candidate.is_dir()
|
not candidate.is_dir()
|
||||||
or candidate.is_symlink()
|
or candidate.is_symlink()
|
||||||
or RESULT_ID.fullmatch(candidate.name) is None
|
or pattern.fullmatch(candidate.name) is None
|
||||||
):
|
):
|
||||||
raise RuntimeError("M4.8T result candidate is invalid")
|
raise RuntimeError("M4.8 result candidate is invalid")
|
||||||
try:
|
try:
|
||||||
verify_laboratory_evidence_result(_DEFINITION, candidate)
|
verify_laboratory_evidence_result(definition, candidate)
|
||||||
manifest = _read_object(candidate / "manifest.json")
|
manifest = _read_object(candidate / "manifest.json")
|
||||||
report = _read_object(candidate / "report.json")
|
report = _read_object(candidate / "report.json")
|
||||||
catalog = _read_object(candidate / "catalog.json")
|
catalog = _read_object(candidate / "catalog.json")
|
||||||
except (LaboratoryEvidenceReportError, OSError, ValueError) as exc:
|
except (LaboratoryEvidenceReportError, OSError, ValueError) as exc:
|
||||||
raise RuntimeError("M4.8T result integrity failed") from exc
|
raise RuntimeError("M4.8 result integrity failed") from exc
|
||||||
|
if variant == "native":
|
||||||
|
_validate_native(candidate, manifest, report, catalog)
|
||||||
|
else:
|
||||||
|
_validate_legacy(candidate, manifest, report, catalog)
|
||||||
|
return {
|
||||||
|
"root": candidate,
|
||||||
|
"manifest": manifest,
|
||||||
|
"report": report,
|
||||||
|
"catalog": catalog,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_legacy(
|
||||||
|
candidate: Path,
|
||||||
|
manifest: dict[str, Any],
|
||||||
|
report: dict[str, Any],
|
||||||
|
catalog: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
identity = manifest.get("identity")
|
identity = manifest.get("identity")
|
||||||
if (
|
if (
|
||||||
manifest.get("schema_version") != LAB_SCHEMA
|
manifest.get("schema_version") != LEGACY_LAB_SCHEMA
|
||||||
or manifest.get("result_id") != candidate.name
|
or manifest.get("result_id") != candidate.name
|
||||||
or manifest.get("status")
|
or manifest.get("status") != "complete-quality-gate-failed-temporal-invariant-passed"
|
||||||
!= "complete-quality-gate-failed-temporal-invariant-passed"
|
|
||||||
or manifest.get("completed") is not True
|
or manifest.get("completed") is not True
|
||||||
or manifest.get("bounded_question_accepted") is not False
|
or manifest.get("bounded_question_accepted") is not False
|
||||||
or manifest.get("ground_truth") is not False
|
or manifest.get("ground_truth") is not False
|
||||||
@@ -194,47 +333,143 @@ def _load_result(candidate: Path) -> dict[str, Any]:
|
|||||||
or not candidate.name.endswith(str(manifest.get("identity_sha256")))
|
or not candidate.name.endswith(str(manifest.get("identity_sha256")))
|
||||||
or identity.get("authority") != false_authority()
|
or identity.get("authority") != false_authority()
|
||||||
or manifest.get("authority") != false_authority()
|
or manifest.get("authority") != false_authority()
|
||||||
or report.get("schema_version") != REPORT_SCHEMA
|
or report.get("schema_version") != LEGACY_REPORT_SCHEMA
|
||||||
or report.get("result_id") != candidate.name
|
or report.get("result_id") != candidate.name
|
||||||
or report.get("authority") != false_authority()
|
or report.get("authority") != false_authority()
|
||||||
or report.get("decision", {}).get("quality_accepted") is not False
|
or report.get("decision", {}).get("quality_accepted") is not False
|
||||||
or report.get("decision", {}).get("temporal_invariant_passed") is not True
|
or report.get("decision", {}).get("temporal_invariant_passed") is not True
|
||||||
or catalog.get("schema_version") != CATALOG_SCHEMA
|
or catalog.get("schema_version") != LEGACY_CATALOG_SCHEMA
|
||||||
or catalog.get("result_id") != candidate.name
|
or catalog.get("result_id") != candidate.name
|
||||||
or catalog.get("case_count") != 16
|
or catalog.get("case_count") != 16
|
||||||
or not isinstance(catalog.get("cases"), list)
|
or not isinstance(catalog.get("cases"), list)
|
||||||
or len(catalog["cases"]) != 16
|
or len(catalog["cases"]) != 16
|
||||||
or any(not _valid_case(item) for item in catalog["cases"])
|
or any(not _valid_legacy_case(item) for item in catalog["cases"])
|
||||||
):
|
):
|
||||||
raise RuntimeError("M4.8T result contract changed")
|
raise RuntimeError("M4.8T result contract changed")
|
||||||
return {"root": candidate, "manifest": manifest, "report": report, "catalog": catalog}
|
|
||||||
|
|
||||||
|
|
||||||
def _valid_case(value: object) -> bool:
|
def _validate_native(
|
||||||
|
candidate: Path,
|
||||||
|
manifest: dict[str, Any],
|
||||||
|
report: dict[str, Any],
|
||||||
|
catalog: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
identity = manifest.get("identity")
|
||||||
|
cases = catalog.get("cases")
|
||||||
|
if (
|
||||||
|
manifest.get("schema_version") != NATIVE_LAB_SCHEMA
|
||||||
|
or manifest.get("result_id") != candidate.name
|
||||||
|
or manifest.get("status") != "complete-review-ready-quality-not-adjudicated"
|
||||||
|
or manifest.get("completed") is not True
|
||||||
|
or manifest.get("bounded_question_accepted") is not True
|
||||||
|
or manifest.get("ground_truth") is not False
|
||||||
|
or not isinstance(manifest.get("created_at_utc"), str)
|
||||||
|
or not isinstance(identity, dict)
|
||||||
|
or manifest.get("identity_sha256") != _canonical_sha256(identity)
|
||||||
|
or not candidate.name.endswith(str(manifest.get("identity_sha256")))
|
||||||
|
or identity.get("authority") != false_authority()
|
||||||
|
or manifest.get("authority") != false_authority()
|
||||||
|
or report.get("schema_version") != NATIVE_REPORT_SCHEMA
|
||||||
|
or report.get("result_id") != candidate.name
|
||||||
|
or report.get("authority") != false_authority()
|
||||||
|
or report.get("decision", {}).get("review_ready") is not True
|
||||||
|
or report.get("decision", {}).get("quality_evaluated") is not False
|
||||||
|
or report.get("decision", {}).get("candidate_accepted") is not False
|
||||||
|
or catalog.get("schema_version") != NATIVE_CATALOG_SCHEMA
|
||||||
|
or catalog.get("result_id") != candidate.name
|
||||||
|
or catalog.get("case_count") != 24
|
||||||
|
or catalog.get("ground_truth") is not False
|
||||||
|
or not isinstance(cases, list)
|
||||||
|
or len(cases) != 24
|
||||||
|
or any(not _valid_native_case(item) for item in cases)
|
||||||
|
):
|
||||||
|
raise RuntimeError("M4.8Q result contract changed")
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_legacy_case(value: object) -> bool:
|
||||||
return (
|
return (
|
||||||
isinstance(value, dict)
|
isinstance(value, dict)
|
||||||
and isinstance(value.get("case_id"), str)
|
and isinstance(value.get("case_id"), str)
|
||||||
and CASE_ID.fullmatch(value["case_id"]) is not None
|
and LEGACY_CASE_ID.fullmatch(value["case_id"]) is not None
|
||||||
and value.get("image_id") == int(value["case_id"])
|
and value.get("image_id") == int(value["case_id"])
|
||||||
and value.get("path") == f"review/review-{value['case_id']}.jpg"
|
and value.get("path") == f"review/review-{value['case_id']}.jpg"
|
||||||
and value.get("media_type") == "image/jpeg"
|
and value.get("media_type") == "image/jpeg"
|
||||||
and isinstance(value.get("byte_length"), int)
|
and _valid_file_proof(value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_native_case(value: object) -> bool:
|
||||||
|
if not isinstance(value, dict) or not isinstance(value.get("case_id"), str):
|
||||||
|
return False
|
||||||
|
case_id = value["case_id"]
|
||||||
|
proposals = value.get("proposals")
|
||||||
|
return (
|
||||||
|
NATIVE_CASE_ID.fullmatch(case_id) is not None
|
||||||
|
and value.get("sequence") == int(case_id)
|
||||||
|
and value.get("frame_id") == f"frame-{case_id}"
|
||||||
|
and isinstance(value.get("evidence_time_ns"), int)
|
||||||
|
and value.get("path") == f"cases/frame-{case_id}.jpg"
|
||||||
|
and value.get("media_type") == "image/jpeg"
|
||||||
|
and value.get("width") == 800
|
||||||
|
and value.get("height") == 600
|
||||||
|
and value.get("geometric_resampling") is False
|
||||||
|
and isinstance(value.get("selection_buckets"), list)
|
||||||
|
and isinstance(value.get("comparison"), dict)
|
||||||
|
and isinstance(proposals, list)
|
||||||
|
and len(proposals) > 0
|
||||||
|
and all(_valid_native_proposal(item) for item in proposals)
|
||||||
|
and _valid_file_proof(value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_native_proposal(value: object) -> bool:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return False
|
||||||
|
box = value.get("box_xyxy")
|
||||||
|
return (
|
||||||
|
isinstance(value.get("proposal_id"), str)
|
||||||
|
and isinstance(value.get("class_name"), str)
|
||||||
|
and isinstance(value.get("risk_family"), str)
|
||||||
|
and isinstance(value.get("score"), (int, float))
|
||||||
|
and not isinstance(value.get("score"), bool)
|
||||||
|
and 0.25 <= value["score"] <= 1
|
||||||
|
and isinstance(box, list)
|
||||||
|
and len(box) == 4
|
||||||
|
and all(isinstance(item, (int, float)) and not isinstance(item, bool) for item in box)
|
||||||
|
and 0 <= box[0] < box[2] <= 800
|
||||||
|
and 0 <= box[1] < box[3] <= 600
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_file_proof(value: dict[str, Any]) -> bool:
|
||||||
|
return (
|
||||||
|
isinstance(value.get("byte_length"), int)
|
||||||
and value["byte_length"] > 0
|
and value["byte_length"] > 0
|
||||||
and isinstance(value.get("sha256"), str)
|
and isinstance(value.get("sha256"), str)
|
||||||
and re.fullmatch(r"[a-f0-9]{64}", value["sha256"]) is not None
|
and re.fullmatch(r"[a-f0-9]{64}", value["sha256"]) is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
|
def _resolve_candidate(
|
||||||
if RESULT_ID.fullmatch(result_id) is None:
|
legacy_provider: RootProvider,
|
||||||
raise HTTPException(status_code=404, detail="M4.8T result not found")
|
native_provider: RootProvider,
|
||||||
|
result_id: str,
|
||||||
|
) -> tuple[Path, Variant]:
|
||||||
|
provider: RootProvider
|
||||||
|
variant: Variant
|
||||||
|
if NATIVE_RESULT_ID.fullmatch(result_id) is not None:
|
||||||
|
provider, variant = native_provider, "native"
|
||||||
|
elif LEGACY_RESULT_ID.fullmatch(result_id) is not None:
|
||||||
|
provider, variant = legacy_provider, "legacy"
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="M4.8 result not found")
|
||||||
root = _configured_root(provider)
|
root = _configured_root(provider)
|
||||||
if root is None:
|
if root is None:
|
||||||
raise HTTPException(status_code=404, detail="M4.8T result not found")
|
raise HTTPException(status_code=404, detail="M4.8 result not found")
|
||||||
candidate = (root / result_id).resolve()
|
candidate = (root / result_id).resolve()
|
||||||
if candidate.parent != root or candidate.is_symlink() or not candidate.is_dir():
|
if candidate.parent != root or candidate.is_symlink() or not candidate.is_dir():
|
||||||
raise HTTPException(status_code=404, detail="M4.8T result not found")
|
raise HTTPException(status_code=404, detail="M4.8 result not found")
|
||||||
return candidate
|
return candidate, variant
|
||||||
|
|
||||||
|
|
||||||
def _configured_root(provider: RootProvider) -> Path | None:
|
def _configured_root(provider: RootProvider) -> Path | None:
|
||||||
@@ -251,29 +486,18 @@ def _configured_root(provider: RootProvider) -> Path | None:
|
|||||||
return root if root.is_dir() else None
|
return root if root.is_dir() else None
|
||||||
|
|
||||||
|
|
||||||
def _candidates(root: Path) -> list[Path]:
|
def _candidates(root: Path, pattern: re.Pattern[str]) -> list[Path]:
|
||||||
return sorted(
|
return sorted(
|
||||||
(
|
(
|
||||||
item
|
item
|
||||||
for item in root.iterdir()
|
for item in root.iterdir()
|
||||||
if item.is_dir() and not item.is_symlink() and RESULT_ID.fullmatch(item.name)
|
if item.is_dir() and not item.is_symlink() and pattern.fullmatch(item.name)
|
||||||
),
|
),
|
||||||
key=lambda item: item.stat().st_mtime_ns,
|
key=lambda item: item.stat().st_mtime_ns,
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _empty_catalog(configured: bool) -> dict[str, object]:
|
|
||||||
return {
|
|
||||||
"schema_version": CATALOG_VIEW_SCHEMA,
|
|
||||||
"configured": configured,
|
|
||||||
"items": [],
|
|
||||||
"candidate_total": 0,
|
|
||||||
"invalid_total": 0,
|
|
||||||
"access": "read-only",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _read_object(path: Path) -> dict[str, Any]:
|
def _read_object(path: Path) -> dict[str, Any]:
|
||||||
value = json.loads(path.read_text("utf-8"))
|
value = json.loads(path.read_text("utf-8"))
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
|
|||||||
@@ -152,6 +152,15 @@ def test_profile_is_strict_digest_bound_and_store_accepts_exact_evidence() -> No
|
|||||||
assert store.profile.local_surface_sha256 == (
|
assert store.profile.local_surface_sha256 == (
|
||||||
"f57eb2485b6cef47f2a97a2d9ff1aa9fd9265fe1eb69cd5852d12f39e13b8bc6"
|
"f57eb2485b6cef47f2a97a2d9ff1aa9fd9265fe1eb69cd5852d12f39e13b8bc6"
|
||||||
)
|
)
|
||||||
|
step_candidates = store.point_step_candidates_for_frame(0)
|
||||||
|
assert step_candidates is not None
|
||||||
|
assert step_candidates.shape == (2389,)
|
||||||
|
assert step_candidates.dtype == np.uint8
|
||||||
|
assert step_candidates.flags.writeable is False
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
step_candidates[0] = 0
|
||||||
|
with pytest.raises(GeometryProviderError, match="frame index"):
|
||||||
|
store.point_step_candidates_for_frame(True)
|
||||||
|
|
||||||
|
|
||||||
def test_provider_arbitrates_points_and_publishes_classless_geometry_only() -> None:
|
def test_provider_arbitrates_points_and_publishes_classless_geometry_only() -> None:
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
|||||||
repository_root / "config" / "laboratories"
|
repository_root / "config" / "laboratories"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(registry.definitions) == 38
|
assert len(registry.definitions) == 39
|
||||||
assert {item.work_id for item in registry.definitions} >= {
|
assert {item.work_id for item in registry.definitions} >= {
|
||||||
"e31-source-binding",
|
"e31-source-binding",
|
||||||
"e46j-raw-fisheye-realtime",
|
"e46j-raw-fisheye-realtime",
|
||||||
@@ -141,15 +141,26 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
|||||||
"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",
|
||||||
"m48s-fixed-class-detector",
|
"m48s-fixed-class-detector",
|
||||||
"m48t-risk-quality-temporal",
|
"m48t-risk-quality-temporal",
|
||||||
}
|
}
|
||||||
m48 = next(
|
m48 = next(
|
||||||
item for item in registry.definitions
|
item for item in registry.definitions if item.work_id == "m48-object-centric-quality"
|
||||||
if item.work_id == "m48-object-centric-quality"
|
|
||||||
)
|
)
|
||||||
assert [variant.phase for variant in m48.evidence_variants] == ["review", "result"]
|
assert [variant.phase for variant in m48.evidence_variants] == ["review", "result"]
|
||||||
assert [variant.result_id_prefix for variant in m48.evidence_variants] == [
|
assert [variant.result_id_prefix for variant in m48.evidence_variants] == [
|
||||||
"m48-object-quality-pack",
|
"m48-object-quality-pack",
|
||||||
"m48-object-quality-result",
|
"m48-object-quality-result",
|
||||||
]
|
]
|
||||||
|
m48t = next(
|
||||||
|
item for item in registry.definitions if item.work_id == "m48t-risk-quality-temporal"
|
||||||
|
)
|
||||||
|
assert [variant.phase for variant in m48t.evidence_variants] == [
|
||||||
|
"legacy-quality",
|
||||||
|
"result",
|
||||||
|
]
|
||||||
|
assert [variant.result_id_prefix for variant in m48t.evidence_variants] == [
|
||||||
|
"m48t-risk-quality-temporal-lab",
|
||||||
|
"m48q-native-risk-quality-lab",
|
||||||
|
]
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
|||||||
|
|
||||||
assert {row.work_id for row in execution.definitions} == {
|
assert {row.work_id for row in execution.definitions} == {
|
||||||
"m48-small-static-passage-regression",
|
"m48-small-static-passage-regression",
|
||||||
|
"m48-static-occupancy-qualification",
|
||||||
"m48-object-centric-quality",
|
"m48-object-centric-quality",
|
||||||
"m4-replay-threat",
|
"m4-replay-threat",
|
||||||
"e33-worker-shadow",
|
"e33-worker-shadow",
|
||||||
@@ -108,6 +109,9 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
|
|||||||
assert by_work_id["m48-object-centric-quality"].evidence_contract == (
|
assert by_work_id["m48-object-centric-quality"].evidence_contract == (
|
||||||
"missioncore.m48-object-centric-quality-result/v1"
|
"missioncore.m48-object-centric-quality-result/v1"
|
||||||
)
|
)
|
||||||
|
assert by_work_id["m48-static-occupancy-qualification"].evidence_contract == (
|
||||||
|
"missioncore.m48-static-occupancy-qualification-result/v1"
|
||||||
|
)
|
||||||
assert by_work_id["e47-semantic-slam-shadow"].lifecycle == "experimental"
|
assert by_work_id["e47-semantic-slam-shadow"].lifecycle == "experimental"
|
||||||
assert by_work_id["e47-semantic-slam-shadow"].isolation == "bounded-adapter"
|
assert by_work_id["e47-semantic-slam-shadow"].isolation == "bounded-adapter"
|
||||||
assert by_work_id["m48s-fixed-class-detector"].lifecycle == "experimental"
|
assert by_work_id["m48s-fixed-class-detector"].lifecycle == "experimental"
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,82 @@ def _fixture(
|
|||||||
lambda _: regression_result,
|
lambda _: regression_result,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
static_occupancy_result_id = f"m48-static-occupancy-qualification-{'e' * 64}"
|
||||||
|
static_occupancy_root = tmp_path / "static-occupancy" / static_occupancy_result_id
|
||||||
|
static_occupancy_root.mkdir(parents=True)
|
||||||
|
static_occupancy_result = SimpleNamespace(
|
||||||
|
result_id=static_occupancy_result_id,
|
||||||
|
result_root=static_occupancy_root,
|
||||||
|
manifest={
|
||||||
|
"created_at_utc": "2026-08-26T12:00:00Z",
|
||||||
|
"accepted": False,
|
||||||
|
},
|
||||||
|
report={
|
||||||
|
"source": {
|
||||||
|
"m47_lab_result_id": f"m47-reference-graph-lab-{'c' * 64}",
|
||||||
|
"small_static_result_id": regression_result_id,
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"run_label": "M4.8R2",
|
||||||
|
"pipeline_id": "m4-current-rolling-plus-step-static-occupancy/v1",
|
||||||
|
"experiment_id": "m48-static-occupancy-qualification/v1",
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"operator_static_anchor_count": 1,
|
||||||
|
"baseline_qualified_count": 0,
|
||||||
|
"candidate_qualified_count": 1,
|
||||||
|
"unresolved_unknown_count": 0,
|
||||||
|
"critical_near_anchor_count": 1,
|
||||||
|
"critical_near_baseline_recall": 0.0,
|
||||||
|
"critical_near_candidate_recall": 1.0,
|
||||||
|
"approach_anchor_count": 0,
|
||||||
|
"approach_baseline_recall": 0.0,
|
||||||
|
"approach_candidate_recall": 0.0,
|
||||||
|
"canonical_engineering_anchor_count": 4,
|
||||||
|
"canonical_engineering_recall": 1.0,
|
||||||
|
"false_free_count": 0,
|
||||||
|
},
|
||||||
|
"gates": {
|
||||||
|
"critical_near_candidate_recall": True,
|
||||||
|
"approach_candidate_recall": False,
|
||||||
|
"canonical_engineering_recall": True,
|
||||||
|
"zero_false_free": True,
|
||||||
|
"independent_truth_available": False,
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"state": "partial-static-occupancy-qualification",
|
||||||
|
"critical_near_candidate_ready_for_shadow": True,
|
||||||
|
"production_accepted": False,
|
||||||
|
"summary": "fixture",
|
||||||
|
"next_action": "worker shadow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cases=(
|
||||||
|
{
|
||||||
|
"anchor_id": regression_anchor_id,
|
||||||
|
"clip_id": "neutral-clip-01",
|
||||||
|
"sequence": 1,
|
||||||
|
"extent_xyxy": [0.2, 0.2, 0.3, 0.4],
|
||||||
|
"distance_m": 3.0,
|
||||||
|
"distance_band": "critical-near",
|
||||||
|
"accepted_graph": {
|
||||||
|
"matched": False,
|
||||||
|
"component_count": 0,
|
||||||
|
"components": [],
|
||||||
|
"free_space_claimed": False,
|
||||||
|
},
|
||||||
|
"baseline_qualified": False,
|
||||||
|
"candidate_qualified": True,
|
||||||
|
"outcome": "candidate-qualified",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
api,
|
||||||
|
"read_m48_static_occupancy_qualification",
|
||||||
|
lambda _: static_occupancy_result,
|
||||||
|
)
|
||||||
|
|
||||||
observations: dict[str, list[dict[str, Any]]] = {
|
observations: dict[str, list[dict[str, Any]]] = {
|
||||||
"reviews": [],
|
"reviews": [],
|
||||||
"adjudications": [],
|
"adjudications": [],
|
||||||
@@ -444,6 +520,7 @@ def _fixture(
|
|||||||
truth_root_provider=lambda: tmp_path / "truth",
|
truth_root_provider=lambda: tmp_path / "truth",
|
||||||
result_root_provider=lambda: tmp_path / "results",
|
result_root_provider=lambda: tmp_path / "results",
|
||||||
small_static_result_root_provider=lambda: tmp_path / "small-static",
|
small_static_result_root_provider=lambda: tmp_path / "small-static",
|
||||||
|
static_occupancy_result_root_provider=lambda: tmp_path / "static-occupancy",
|
||||||
camera_frame_provider=camera,
|
camera_frame_provider=camera,
|
||||||
camera_playback_provider=camera_playback, # type: ignore[arg-type]
|
camera_playback_provider=camera_playback, # type: ignore[arg-type]
|
||||||
spatial_evidence_provider=spatial_frame if spatial else None,
|
spatial_evidence_provider=spatial_frame if spatial else None,
|
||||||
@@ -1128,3 +1205,31 @@ def test_m48_small_static_regression_is_separate_read_only_assisted_evidence(
|
|||||||
assert body["ground_truth"] is False
|
assert body["ground_truth"] is False
|
||||||
assert body["camera_url"].endswith("/frames/1/camera")
|
assert body["camera_url"].endswith("/frames/1/camera")
|
||||||
assert body["spatial_url"].endswith("/frames/1/spatial")
|
assert body["spatial_url"].endswith("/frames/1/spatial")
|
||||||
|
|
||||||
|
|
||||||
|
def test_m48_static_occupancy_qualification_is_read_only_bounded_evidence(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
client, _, _ = _fixture(tmp_path, monkeypatch, spatial=True)
|
||||||
|
result_id = f"m48-static-occupancy-qualification-{'e' * 64}"
|
||||||
|
|
||||||
|
summary = client.get(
|
||||||
|
f"/api/v1/laboratory/m48/regressions/static-occupancy/{result_id}"
|
||||||
|
)
|
||||||
|
assert summary.status_code == 200
|
||||||
|
assert summary.headers["cache-control"] == "no-store"
|
||||||
|
assert summary.json()["run_label"] == "M4.8R2"
|
||||||
|
assert summary.json()["accepted"] is False
|
||||||
|
assert summary.json()["ground_truth"] is False
|
||||||
|
assert summary.json()["independent_truth"] is False
|
||||||
|
assert summary.json()["metrics"]["critical_near_candidate_recall"] == 1.0
|
||||||
|
assert summary.json()["decision"]["production_accepted"] is False
|
||||||
|
|
||||||
|
catalog = client.get(
|
||||||
|
f"/api/v1/laboratory/m48/regressions/static-occupancy/{result_id}/cases"
|
||||||
|
)
|
||||||
|
assert catalog.status_code == 200
|
||||||
|
assert catalog.json()["case_count"] == 1
|
||||||
|
assert catalog.json()["cases"][0]["outcome"] == "candidate-qualified"
|
||||||
|
assert catalog.json()["cases"][0]["accepted_graph"]["free_space_claimed"] is False
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import k1link.laboratory.m48_static_occupancy_qualification as qualification
|
||||||
|
from k1link.laboratory.evidence_registry import LaboratoryEvidenceRegistry
|
||||||
|
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
AUTHORITY = {
|
||||||
|
"mode": "replay-simulated",
|
||||||
|
"physical_live": False,
|
||||||
|
"commands_enabled": False,
|
||||||
|
"actuation_allowed": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _sealed_result(tmp_path: Path) -> qualification.M48StaticOccupancyQualificationResult:
|
||||||
|
identity = {
|
||||||
|
"schema_version": qualification.M48_STATIC_OCCUPANCY_RESULT_SCHEMA,
|
||||||
|
"human_lab_id": "M4.8R2",
|
||||||
|
"run_label": "fixture",
|
||||||
|
"run_created_at_utc": "2026-08-26T12:00:00Z",
|
||||||
|
"authority": AUTHORITY,
|
||||||
|
}
|
||||||
|
result_id = qualification.M48_STATIC_OCCUPANCY_PREFIX + qualification._canonical_sha256(
|
||||||
|
identity
|
||||||
|
)
|
||||||
|
report = {
|
||||||
|
"schema_version": qualification.M48_STATIC_OCCUPANCY_REPORT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"metrics": {
|
||||||
|
"operator_static_anchor_count": 1,
|
||||||
|
"candidate_qualified_count": 1,
|
||||||
|
},
|
||||||
|
"decision": {"production_accepted": False},
|
||||||
|
"authority": AUTHORITY,
|
||||||
|
}
|
||||||
|
cases = (
|
||||||
|
{
|
||||||
|
"schema_version": qualification.M48_STATIC_OCCUPANCY_CASE_SCHEMA,
|
||||||
|
"anchor_id": "anchor-" + "a" * 24,
|
||||||
|
"sequence": 10,
|
||||||
|
"baseline_qualified": False,
|
||||||
|
"candidate_qualified": True,
|
||||||
|
"outcome": "candidate-qualified",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
canonical = (
|
||||||
|
{
|
||||||
|
"schema_version": qualification.M48_STATIC_OCCUPANCY_CANONICAL_SCHEMA,
|
||||||
|
"anchor_id": "hemisphere-01",
|
||||||
|
"sequence": 1880,
|
||||||
|
"matched": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
destination = tmp_path / "results" / result_id
|
||||||
|
qualification._publish_result(
|
||||||
|
destination,
|
||||||
|
identity,
|
||||||
|
"2026-08-26T12:00:00Z",
|
||||||
|
False,
|
||||||
|
report,
|
||||||
|
cases,
|
||||||
|
canonical,
|
||||||
|
)
|
||||||
|
return qualification.read_m48_static_occupancy_qualification(destination)
|
||||||
|
|
||||||
|
|
||||||
|
def test_seals_bounded_static_occupancy_evidence_without_production_authority(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
result = _sealed_result(tmp_path)
|
||||||
|
|
||||||
|
assert result.manifest["accepted"] is False
|
||||||
|
assert result.manifest["ground_truth"] is False
|
||||||
|
assert result.manifest["authority"] == AUTHORITY
|
||||||
|
assert result.report["decision"]["production_accepted"] is False
|
||||||
|
assert result.cases[0]["outcome"] == "candidate-qualified"
|
||||||
|
|
||||||
|
registry = LaboratoryEvidenceRegistry.from_directory(
|
||||||
|
REPOSITORY_ROOT / "config/laboratories"
|
||||||
|
)
|
||||||
|
definition = next(
|
||||||
|
row
|
||||||
|
for row in registry.definitions
|
||||||
|
if row.work_id == "m48-static-occupancy-qualification"
|
||||||
|
)
|
||||||
|
proof = verify_laboratory_evidence_result(definition, result.result_root)
|
||||||
|
assert proof["result_id"] == result.result_id
|
||||||
|
assert proof["artifact_count"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_reader_rejects_changed_static_occupancy_case_ledger(tmp_path: Path) -> None:
|
||||||
|
result = _sealed_result(tmp_path)
|
||||||
|
cases_path = result.result_root / "cases.jsonl"
|
||||||
|
cases_path.write_bytes(cases_path.read_bytes() + b"{}\n")
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
qualification.M48StaticOccupancyQualificationError,
|
||||||
|
match="artifact proof",
|
||||||
|
):
|
||||||
|
qualification.read_m48_static_occupancy_qualification(result.result_root)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RUNNER_PATH = (
|
||||||
|
REPOSITORY_ROOT / "experiments" / "perception" / "run_m48q_native_risk_case_mining_worker.py"
|
||||||
|
)
|
||||||
|
SPEC = importlib.util.spec_from_file_location("m48q_case_mining", RUNNER_PATH)
|
||||||
|
assert SPEC is not None and SPEC.loader is not None
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
sys.modules[SPEC.name] = MODULE
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
def candidate(sequence: int, *buckets: str):
|
||||||
|
return MODULE.FrameCandidate(
|
||||||
|
sequence=sequence,
|
||||||
|
frame_id=f"frame-{sequence:06d}",
|
||||||
|
evidence_time_ns=sequence * 1_000_000,
|
||||||
|
proposals=(),
|
||||||
|
native_count=0,
|
||||||
|
legacy_count=0,
|
||||||
|
matched_count=0,
|
||||||
|
buckets=frozenset(buckets),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_selection_is_deterministic_balanced_and_separated() -> None:
|
||||||
|
candidates = [
|
||||||
|
candidate(sequence, "person" if sequence % 2 == 0 else "vehicle")
|
||||||
|
for sequence in range(0, 400, 5)
|
||||||
|
]
|
||||||
|
quotas = {"person": 3, "vehicle": 3}
|
||||||
|
|
||||||
|
first = MODULE.select_cases(
|
||||||
|
candidates,
|
||||||
|
bucket_quotas=quotas,
|
||||||
|
minimum_sequence_separation=10,
|
||||||
|
)
|
||||||
|
second = MODULE.select_cases(
|
||||||
|
candidates,
|
||||||
|
bucket_quotas=quotas,
|
||||||
|
minimum_sequence_separation=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [item.sequence for item in first] == [item.sequence for item in second]
|
||||||
|
assert len(first) == 6
|
||||||
|
assert all(
|
||||||
|
abs(left.sequence - right.sequence) >= 10
|
||||||
|
for index, left in enumerate(first)
|
||||||
|
for right in first[index + 1 :]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_selection_refuses_missing_bucket_coverage() -> None:
|
||||||
|
with pytest.raises(MODULE.M48QCaseMiningError, match="animal produced 0/1"):
|
||||||
|
MODULE.select_cases(
|
||||||
|
[candidate(0, "person")],
|
||||||
|
bucket_quotas={"animal": 1},
|
||||||
|
minimum_sequence_separation=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_freezes_raw_raster_and_false_authority() -> None:
|
||||||
|
import json
|
||||||
|
|
||||||
|
profile = json.loads(
|
||||||
|
(
|
||||||
|
REPOSITORY_ROOT / "config" / "perception" / "m48q-native-risk-case-mining-v1.json"
|
||||||
|
).read_text("utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert profile["source"] == {
|
||||||
|
"source_id": "RAVNOVES00",
|
||||||
|
"frame_count": 4489,
|
||||||
|
"raster_width": 800,
|
||||||
|
"raster_height": 600,
|
||||||
|
"video_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8",
|
||||||
|
"geometric_resampling": False,
|
||||||
|
"rectification": False,
|
||||||
|
"warp": False,
|
||||||
|
}
|
||||||
|
assert sum(profile["selection"]["bucket_quotas"].values()) == 24
|
||||||
|
assert profile["scope"]["quality_evaluated"] is False
|
||||||
|
assert profile["authority"] == MODULE.FALSE_AUTHORITY
|
||||||
@@ -6,6 +6,18 @@ from pathlib import Path
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
CATALOG_SCHEMA as NATIVE_CATALOG_SCHEMA,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
LAB_SCHEMA as NATIVE_LAB_SCHEMA,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
REPORT_SCHEMA as NATIVE_REPORT_SCHEMA,
|
||||||
|
)
|
||||||
|
from k1link.laboratory.m48q_native_risk_quality_lab import (
|
||||||
|
RESULT_PREFIX as NATIVE_RESULT_PREFIX,
|
||||||
|
)
|
||||||
from k1link.laboratory.m48t_risk_quality_lab import (
|
from k1link.laboratory.m48t_risk_quality_lab import (
|
||||||
CATALOG_SCHEMA,
|
CATALOG_SCHEMA,
|
||||||
LAB_SCHEMA,
|
LAB_SCHEMA,
|
||||||
@@ -150,3 +162,129 @@ def test_m48t_lab_api_fails_closed_after_visual_tamper(tmp_path: Path) -> None:
|
|||||||
catalog = client.get("/api/v1/laboratory/m48t/risk-quality/results")
|
catalog = client.get("/api/v1/laboratory/m48t/risk-quality/results")
|
||||||
assert catalog.json()["items"] == []
|
assert catalog.json()["items"] == []
|
||||||
assert catalog.json()["invalid_total"] == 1
|
assert catalog.json()["invalid_total"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def _native_fixture(tmp_path: Path) -> tuple[TestClient, Path, str]:
|
||||||
|
root = tmp_path / "native-results"
|
||||||
|
root.mkdir()
|
||||||
|
identity = {"schema_version": NATIVE_LAB_SCHEMA, "authority": false_authority()}
|
||||||
|
identity_sha256 = hashlib.sha256(canonical_json(identity)).hexdigest()
|
||||||
|
result_id = NATIVE_RESULT_PREFIX + identity_sha256
|
||||||
|
result_root = root / result_id
|
||||||
|
cases_root = result_root / "cases"
|
||||||
|
cases_root.mkdir(parents=True)
|
||||||
|
cases = []
|
||||||
|
image_paths = []
|
||||||
|
for index in range(24):
|
||||||
|
case_id = f"{index * 20 + 12:06d}"
|
||||||
|
image_path = cases_root / f"frame-{case_id}.jpg"
|
||||||
|
image_path.write_bytes(b"native-jpeg" + bytes([index]))
|
||||||
|
image_paths.append(image_path)
|
||||||
|
cases.append(
|
||||||
|
{
|
||||||
|
"case_id": case_id,
|
||||||
|
"sequence": int(case_id),
|
||||||
|
"frame_id": f"frame-{case_id}",
|
||||||
|
"evidence_time_ns": 35_000_000_000 + index * 1_000_000,
|
||||||
|
"path": f"cases/{image_path.name}",
|
||||||
|
"media_type": "image/jpeg",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"byte_length": image_path.stat().st_size,
|
||||||
|
"sha256": hashlib.sha256(image_path.read_bytes()).hexdigest(),
|
||||||
|
"geometric_resampling": False,
|
||||||
|
"selection_buckets": ["person"],
|
||||||
|
"comparison": {
|
||||||
|
"native_detection_count": 1,
|
||||||
|
"legacy_704_detection_count": 1,
|
||||||
|
"matched_detection_count_iou_at_least_0_5": 1,
|
||||||
|
},
|
||||||
|
"proposals": [
|
||||||
|
{
|
||||||
|
"proposal_id": f"proposal-{index}-0",
|
||||||
|
"class_name": "person",
|
||||||
|
"risk_family": "person",
|
||||||
|
"score": 0.75,
|
||||||
|
"box_xyxy": [10.0, 20.0, 100.0, 200.0],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
catalog = {
|
||||||
|
"schema_version": NATIVE_CATALOG_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"case_count": 24,
|
||||||
|
"source_raster": {"width": 800, "height": 600},
|
||||||
|
"overlay": {"client_rendered": True, "toggleable": True},
|
||||||
|
"ground_truth": False,
|
||||||
|
"cases": cases,
|
||||||
|
}
|
||||||
|
report = {
|
||||||
|
"schema_version": NATIVE_REPORT_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"source": {},
|
||||||
|
"configuration": {},
|
||||||
|
"method": {},
|
||||||
|
"execution": {},
|
||||||
|
"metrics": {},
|
||||||
|
"acceptance": {},
|
||||||
|
"decision": {
|
||||||
|
"review_ready": True,
|
||||||
|
"quality_evaluated": False,
|
||||||
|
"candidate_accepted": False,
|
||||||
|
},
|
||||||
|
"limitations": [],
|
||||||
|
"authority": false_authority(),
|
||||||
|
}
|
||||||
|
catalog_path = result_root / "catalog.json"
|
||||||
|
report_path = result_root / "report.json"
|
||||||
|
_write_json(catalog_path, catalog)
|
||||||
|
_write_json(report_path, report)
|
||||||
|
artifacts = [
|
||||||
|
_descriptor(catalog_path, result_root, "visual-evidence-catalog", "application/json"),
|
||||||
|
_descriptor(report_path, result_root, "laboratory-report", "application/json"),
|
||||||
|
*[
|
||||||
|
_descriptor(path, result_root, "visual-evidence-native-raw-fisheye-frame", "image/jpeg")
|
||||||
|
for path in image_paths
|
||||||
|
],
|
||||||
|
]
|
||||||
|
manifest = {
|
||||||
|
"schema_version": NATIVE_LAB_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"identity_sha256": identity_sha256,
|
||||||
|
"identity": identity,
|
||||||
|
"created_at_utc": "2026-08-26T09:45:15.574709Z",
|
||||||
|
"status": "complete-review-ready-quality-not-adjudicated",
|
||||||
|
"completed": True,
|
||||||
|
"bounded_question_accepted": True,
|
||||||
|
"ground_truth": False,
|
||||||
|
"authority": false_authority(),
|
||||||
|
"artifacts": artifacts,
|
||||||
|
}
|
||||||
|
_write_json(result_root / "manifest.json", manifest)
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(build_m48t_risk_quality_lab_router(native_root_provider=lambda: root))
|
||||||
|
return TestClient(app), result_root, result_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_m48q_lab_api_projects_native_raw_cases_without_quality_promotion(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
client, result_root, result_id = _native_fixture(tmp_path)
|
||||||
|
|
||||||
|
catalog = client.get("/api/v1/laboratory/m48t/risk-quality/results")
|
||||||
|
assert catalog.status_code == 200
|
||||||
|
assert catalog.json()["items"][0]["variant"] == "native-risk-review"
|
||||||
|
result = client.get(f"/api/v1/laboratory/m48t/risk-quality/results/{result_id}")
|
||||||
|
assert result.status_code == 200
|
||||||
|
assert result.json()["ground_truth"] is False
|
||||||
|
assert result.json()["decision"]["quality_evaluated"] is False
|
||||||
|
assert len(result.json()["review"]["cases"]) == 24
|
||||||
|
assert result.json()["review"]["cases"][0]["geometric_resampling"] is False
|
||||||
|
|
||||||
|
case_id = "000012"
|
||||||
|
image = client.get(
|
||||||
|
f"/api/v1/laboratory/m48t/risk-quality/results/{result_id}/review/{case_id}.jpg"
|
||||||
|
)
|
||||||
|
assert image.status_code == 200
|
||||||
|
assert image.content == (result_root / f"cases/frame-{case_id}.jpg").read_bytes()
|
||||||
|
|||||||
Reference in New Issue
Block a user