|
|
|
@@ -0,0 +1,326 @@
|
|
|
|
|
export interface M48TLaboratoryMethod {
|
|
|
|
|
completeness: "complete";
|
|
|
|
|
executionClass: "hybrid";
|
|
|
|
|
pipelineId: string;
|
|
|
|
|
components: readonly {
|
|
|
|
|
kind: "source" | "model" | "algorithm" | "runtime" | "tool";
|
|
|
|
|
name: string;
|
|
|
|
|
version: string;
|
|
|
|
|
role: string;
|
|
|
|
|
identitySha256: string;
|
|
|
|
|
}[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface M48TReviewCase {
|
|
|
|
|
caseId: string;
|
|
|
|
|
imageId: number;
|
|
|
|
|
imageUrl: string;
|
|
|
|
|
byteLength: number;
|
|
|
|
|
sha256: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface M48TRiskQualityResult {
|
|
|
|
|
resultId: string;
|
|
|
|
|
createdAtUtc: string;
|
|
|
|
|
source: {
|
|
|
|
|
quality: {
|
|
|
|
|
datasetId: "coco-2017-val";
|
|
|
|
|
riskImages: number;
|
|
|
|
|
truthInstances: number;
|
|
|
|
|
independentHumanAnnotations: true;
|
|
|
|
|
ravnovesGroundTruth: false;
|
|
|
|
|
};
|
|
|
|
|
temporal: {
|
|
|
|
|
sourceId: "RAVNOVES00";
|
|
|
|
|
frames: number;
|
|
|
|
|
publications: number;
|
|
|
|
|
independentSemanticTruthAvailable: false;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
candidate: {
|
|
|
|
|
providerId: string;
|
|
|
|
|
modelId: "rf_detr_large:1";
|
|
|
|
|
minimumScore: 0.25;
|
|
|
|
|
};
|
|
|
|
|
execution: {
|
|
|
|
|
durationSeconds: number;
|
|
|
|
|
effectiveImagesPerSecond: number;
|
|
|
|
|
imageP95Ms: number;
|
|
|
|
|
inferenceP95Ms: number;
|
|
|
|
|
timingIsAdmissionEvidence: false;
|
|
|
|
|
};
|
|
|
|
|
quality: {
|
|
|
|
|
microPrecision: number;
|
|
|
|
|
microRecall: number;
|
|
|
|
|
mediumLargeRecall: number;
|
|
|
|
|
emptyRiskImageFraction: number;
|
|
|
|
|
families: Readonly<Record<"person" | "animal" | "light-road-user" | "vehicle", number>>;
|
|
|
|
|
};
|
|
|
|
|
counts: {
|
|
|
|
|
truth: number;
|
|
|
|
|
predictions: number;
|
|
|
|
|
truePositive: number;
|
|
|
|
|
falsePositive: number;
|
|
|
|
|
falseNegative: number;
|
|
|
|
|
};
|
|
|
|
|
failures: Readonly<Record<string, number>>;
|
|
|
|
|
temporal: {
|
|
|
|
|
rawClassSwitches: number;
|
|
|
|
|
stableClassSwitches: number;
|
|
|
|
|
rawFamilySwitches: number;
|
|
|
|
|
stableFamilySwitches: number;
|
|
|
|
|
suppressedClassSwitches: number;
|
|
|
|
|
suppressedFamilySwitches: number;
|
|
|
|
|
selectedPublications: number;
|
|
|
|
|
semanticObservations: number;
|
|
|
|
|
peakActiveComponents: number;
|
|
|
|
|
maximumActiveComponents: number;
|
|
|
|
|
};
|
|
|
|
|
acceptance: {
|
|
|
|
|
qualityPassed: false;
|
|
|
|
|
failedQualityGates: readonly [
|
|
|
|
|
"minimum-micro-precision",
|
|
|
|
|
"minimum-family-recall:vehicle",
|
|
|
|
|
];
|
|
|
|
|
temporalInvariantPassed: true;
|
|
|
|
|
};
|
|
|
|
|
review: {
|
|
|
|
|
truthColor: "green";
|
|
|
|
|
predictionColor: "yellow";
|
|
|
|
|
cases: readonly M48TReviewCase[];
|
|
|
|
|
};
|
|
|
|
|
method: M48TLaboratoryMethod;
|
|
|
|
|
limitations: readonly string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
|
|
|
|
|
|
|
|
export class M48TContractError extends Error {}
|
|
|
|
|
|
|
|
|
|
function object(value: unknown, label: string): Record<string, unknown> {
|
|
|
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
|
|
|
throw new M48TContractError(`${label}: ожидался объект.`);
|
|
|
|
|
}
|
|
|
|
|
return value as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function array(value: unknown, label: string): readonly unknown[] {
|
|
|
|
|
if (!Array.isArray(value)) throw new M48TContractError(`${label}: ожидался массив.`);
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function text(value: unknown, label: string): string {
|
|
|
|
|
if (typeof value !== "string" || !value.trim()) {
|
|
|
|
|
throw new M48TContractError(`${label}: ожидалась строка.`);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function number(value: unknown, label: string): number {
|
|
|
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
|
|
|
throw new M48TContractError(`${label}: ожидалось неотрицательное число.`);
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function integer(value: unknown, label: string): number {
|
|
|
|
|
const parsed = number(value, label);
|
|
|
|
|
if (!Number.isSafeInteger(parsed)) {
|
|
|
|
|
throw new M48TContractError(`${label}: ожидалось целое число.`);
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function exact<T extends string | number | boolean>(
|
|
|
|
|
value: unknown,
|
|
|
|
|
expected: T,
|
|
|
|
|
label: string,
|
|
|
|
|
): T {
|
|
|
|
|
if (value !== expected) throw new M48TContractError(`${label}: нарушен контракт.`);
|
|
|
|
|
return expected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sha(value: unknown, label: string): string {
|
|
|
|
|
const parsed = text(value, label);
|
|
|
|
|
if (!/^[a-f0-9]{64}$/.test(parsed)) {
|
|
|
|
|
throw new M48TContractError(`${label}: нарушена SHA-256 идентичность.`);
|
|
|
|
|
}
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function method(value: unknown): M48TLaboratoryMethod {
|
|
|
|
|
const raw = object(value, "M4.8T method");
|
|
|
|
|
exact(raw.schema_version, "missioncore.laboratory-method/v1", "M4.8T method schema");
|
|
|
|
|
exact(raw.completeness, "complete", "M4.8T method completeness");
|
|
|
|
|
exact(raw.execution_class, "hybrid", "M4.8T method execution");
|
|
|
|
|
const allowedKinds = new Set(["source", "model", "algorithm", "runtime", "tool"]);
|
|
|
|
|
return {
|
|
|
|
|
completeness: "complete",
|
|
|
|
|
executionClass: "hybrid",
|
|
|
|
|
pipelineId: text(raw.pipeline_id, "M4.8T pipeline"),
|
|
|
|
|
components: array(raw.components, "M4.8T components").map((value) => {
|
|
|
|
|
const component = object(value, "M4.8T component");
|
|
|
|
|
const kind = text(component.kind, "M4.8T component kind");
|
|
|
|
|
if (!allowedKinds.has(kind)) throw new M48TContractError("M4.8T component kind: неизвестное значение.");
|
|
|
|
|
return {
|
|
|
|
|
kind: kind as "source" | "model" | "algorithm" | "runtime" | "tool",
|
|
|
|
|
name: text(component.name, "M4.8T component name"),
|
|
|
|
|
version: text(component.version, "M4.8T component version"),
|
|
|
|
|
role: text(component.role, "M4.8T component role"),
|
|
|
|
|
identitySha256: sha(component.identity_sha256, "M4.8T component identity"),
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseResult(value: unknown, expectedResultId: string): M48TRiskQualityResult {
|
|
|
|
|
const raw = object(value, "M4.8T result");
|
|
|
|
|
exact(raw.schema_version, "missioncore.m48t-risk-quality-temporal-view/v1", "M4.8T schema");
|
|
|
|
|
exact(raw.result_id, expectedResultId, "M4.8T identity");
|
|
|
|
|
exact(raw.status, "complete-quality-gate-failed-temporal-invariant-passed", "M4.8T status");
|
|
|
|
|
exact(raw.access, "read-only", "M4.8T access");
|
|
|
|
|
exact(raw.ground_truth, false, "M4.8T ground truth");
|
|
|
|
|
|
|
|
|
|
const source = object(raw.source, "M4.8T source");
|
|
|
|
|
const qualitySource = object(source.quality, "M4.8T quality source");
|
|
|
|
|
const temporalSource = object(source.temporal, "M4.8T temporal source");
|
|
|
|
|
const configuration = object(raw.configuration, "M4.8T configuration");
|
|
|
|
|
const candidate = object(configuration.candidate, "M4.8T candidate");
|
|
|
|
|
const execution = object(raw.execution, "M4.8T execution");
|
|
|
|
|
const imageTiming = object(execution.image_timing_ms, "M4.8T image timing");
|
|
|
|
|
const inferenceTiming = object(execution.triton_inference_ms, "M4.8T inference timing");
|
|
|
|
|
const metrics = object(raw.metrics, "M4.8T metrics");
|
|
|
|
|
const quality = object(metrics.quality, "M4.8T quality metrics");
|
|
|
|
|
const families = object(quality.families, "M4.8T family metrics");
|
|
|
|
|
const familyRecall = (name: "person" | "animal" | "light-road-user" | "vehicle") =>
|
|
|
|
|
number(object(families[name], `M4.8T ${name}`).family_recall, `M4.8T ${name} recall`);
|
|
|
|
|
const counts = object(metrics.counts, "M4.8T counts");
|
|
|
|
|
const failures = object(metrics.failure_buckets, "M4.8T failures");
|
|
|
|
|
const temporal = object(metrics.temporal, "M4.8T temporal metrics");
|
|
|
|
|
const snapshot = object(temporal.snapshot, "M4.8T temporal snapshot");
|
|
|
|
|
const acceptance = object(raw.acceptance, "M4.8T acceptance");
|
|
|
|
|
const qualityGate = object(acceptance.quality, "M4.8T quality gate");
|
|
|
|
|
const temporalGate = object(acceptance.temporal_invariant, "M4.8T temporal gate");
|
|
|
|
|
const failed = array(qualityGate.failed, "M4.8T failed gates").map((item) => text(item, "M4.8T failed gate"));
|
|
|
|
|
if (failed.length !== 2 || failed[0] !== "minimum-micro-precision" || failed[1] !== "minimum-family-recall:vehicle") {
|
|
|
|
|
throw new M48TContractError("M4.8T failed gates: изменён зафиксированный результат.");
|
|
|
|
|
}
|
|
|
|
|
exact(qualityGate.passed, false, "M4.8T quality gate");
|
|
|
|
|
exact(temporalGate.passed, true, "M4.8T temporal invariant");
|
|
|
|
|
exact(temporalGate.semantic_quality_accepted, false, "M4.8T temporal semantic acceptance");
|
|
|
|
|
|
|
|
|
|
const review = object(raw.review, "M4.8T review");
|
|
|
|
|
const legend = object(review.legend, "M4.8T review legend");
|
|
|
|
|
exact(legend.ground_truth, "green", "M4.8T truth legend");
|
|
|
|
|
exact(legend.rf_detr_prediction, "yellow", "M4.8T prediction legend");
|
|
|
|
|
const cases = array(review.cases, "M4.8T review cases").map((value) => {
|
|
|
|
|
const item = object(value, "M4.8T review case");
|
|
|
|
|
const caseId = text(item.case_id, "M4.8T case id");
|
|
|
|
|
if (!/^[0-9]{12}$/.test(caseId)) throw new M48TContractError("M4.8T case identity нарушена.");
|
|
|
|
|
exact(item.media_type, "image/jpeg", "M4.8T case media");
|
|
|
|
|
return {
|
|
|
|
|
caseId,
|
|
|
|
|
imageId: integer(item.image_id, "M4.8T image id"),
|
|
|
|
|
imageUrl: text(item.image_url, "M4.8T image URL"),
|
|
|
|
|
byteLength: integer(item.byte_length, "M4.8T image bytes"),
|
|
|
|
|
sha256: sha(item.sha256, "M4.8T image SHA"),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
if (cases.length !== 16 || new Set(cases.map(({ caseId }) => caseId)).size !== 16) {
|
|
|
|
|
throw new M48TContractError("M4.8T review catalog: нарушен размер.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const parsedFailures: Record<string, number> = {};
|
|
|
|
|
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");
|
|
|
|
|
return {
|
|
|
|
|
resultId: expectedResultId,
|
|
|
|
|
createdAtUtc: text(raw.created_at_utc, "M4.8T created at"),
|
|
|
|
|
source: {
|
|
|
|
|
quality: {
|
|
|
|
|
datasetId: exact(qualitySource.dataset_id, "coco-2017-val", "M4.8T dataset"),
|
|
|
|
|
riskImages: integer(qualitySource.risk_images, "M4.8T risk images"),
|
|
|
|
|
truthInstances: integer(qualitySource.truth_instances, "M4.8T truth instances"),
|
|
|
|
|
independentHumanAnnotations: exact(qualitySource.independent_human_annotations, true, "M4.8T independent truth"),
|
|
|
|
|
ravnovesGroundTruth: exact(qualitySource.ravnoves_ground_truth, false, "M4.8T RAVNOVES truth"),
|
|
|
|
|
},
|
|
|
|
|
temporal: {
|
|
|
|
|
sourceId: exact(temporalSource.source_id, "RAVNOVES00", "M4.8T temporal source"),
|
|
|
|
|
frames: integer(temporalSource.frames, "M4.8T temporal frames"),
|
|
|
|
|
publications: integer(temporalSource.publications, "M4.8T temporal publications"),
|
|
|
|
|
independentSemanticTruthAvailable: exact(temporalSource.independent_semantic_truth_available, false, "M4.8T temporal truth"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
candidate: {
|
|
|
|
|
providerId: text(candidate.provider_id, "M4.8T provider"),
|
|
|
|
|
modelId: exact(candidate.model_id, "rf_detr_large:1", "M4.8T model"),
|
|
|
|
|
minimumScore: exact(candidate.minimum_score, 0.25, "M4.8T threshold"),
|
|
|
|
|
},
|
|
|
|
|
execution: {
|
|
|
|
|
durationSeconds: number(execution.duration_seconds, "M4.8T duration"),
|
|
|
|
|
effectiveImagesPerSecond: number(execution.effective_images_per_second, "M4.8T throughput"),
|
|
|
|
|
imageP95Ms: number(imageTiming.p95, "M4.8T image p95"),
|
|
|
|
|
inferenceP95Ms: number(inferenceTiming.p95, "M4.8T inference p95"),
|
|
|
|
|
timingIsAdmissionEvidence: exact(execution.timing_is_admission_evidence, false, "M4.8T timing authority"),
|
|
|
|
|
},
|
|
|
|
|
quality: {
|
|
|
|
|
microPrecision: number(quality.micro_precision, "M4.8T precision"),
|
|
|
|
|
microRecall: number(quality.micro_recall, "M4.8T recall"),
|
|
|
|
|
mediumLargeRecall: number(quality.medium_large_recall, "M4.8T medium-large recall"),
|
|
|
|
|
emptyRiskImageFraction: number(quality.empty_prediction_risk_image_fraction, "M4.8T empty fraction"),
|
|
|
|
|
families: {
|
|
|
|
|
person: familyRecall("person"),
|
|
|
|
|
animal: familyRecall("animal"),
|
|
|
|
|
"light-road-user": familyRecall("light-road-user"),
|
|
|
|
|
vehicle: familyRecall("vehicle"),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
counts: {
|
|
|
|
|
truth: integer(qualitySource.truth_instances, "M4.8T truth count"),
|
|
|
|
|
predictions: integer(counts.predictions, "M4.8T predictions"),
|
|
|
|
|
truePositive: integer(counts.true_positive, "M4.8T TP"),
|
|
|
|
|
falsePositive: integer(counts.false_positive, "M4.8T FP"),
|
|
|
|
|
falseNegative: integer(counts.false_negative, "M4.8T FN"),
|
|
|
|
|
},
|
|
|
|
|
failures: parsedFailures,
|
|
|
|
|
temporal: {
|
|
|
|
|
rawClassSwitches: integer(temporal.raw_class_switches, "M4.8T raw class switches"),
|
|
|
|
|
stableClassSwitches: integer(temporal.stable_class_switches, "M4.8T stable class switches"),
|
|
|
|
|
rawFamilySwitches: integer(temporal.raw_family_switches, "M4.8T raw family switches"),
|
|
|
|
|
stableFamilySwitches: integer(temporal.stable_family_switches, "M4.8T stable family switches"),
|
|
|
|
|
suppressedClassSwitches: integer(temporal.suppressed_or_deferred_class_switches, "M4.8T suppressed class switches"),
|
|
|
|
|
suppressedFamilySwitches: integer(temporal.suppressed_or_deferred_family_switches, "M4.8T suppressed family switches"),
|
|
|
|
|
selectedPublications: integer(temporal.selected_publications, "M4.8T selected publications"),
|
|
|
|
|
semanticObservations: integer(temporal.semantic_current_observations, "M4.8T semantic observations"),
|
|
|
|
|
peakActiveComponents: integer(snapshot.peak_active_components, "M4.8T active peak"),
|
|
|
|
|
maximumActiveComponents: integer(temporalConfiguration.maximum_active_components, "M4.8T active bound"),
|
|
|
|
|
},
|
|
|
|
|
acceptance: {
|
|
|
|
|
qualityPassed: false,
|
|
|
|
|
failedQualityGates: ["minimum-micro-precision", "minimum-family-recall:vehicle"],
|
|
|
|
|
temporalInvariantPassed: true,
|
|
|
|
|
},
|
|
|
|
|
review: { truthColor: "green", predictionColor: "yellow", cases },
|
|
|
|
|
method: method(raw.method),
|
|
|
|
|
limitations: array(raw.limitations, "M4.8T limitations").map((item) => text(item, "M4.8T limitation")),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchM48TRiskQualityResult(
|
|
|
|
|
resultId: string,
|
|
|
|
|
{
|
|
|
|
|
fetcher = fetch,
|
|
|
|
|
signal,
|
|
|
|
|
}: { fetcher?: LaboratoryFetch; signal?: AbortSignal } = {},
|
|
|
|
|
): Promise<M48TRiskQualityResult> {
|
|
|
|
|
if (!/^m48t-risk-quality-temporal-lab-[a-f0-9]{64}$/.test(resultId)) {
|
|
|
|
|
throw new M48TContractError("M4.8T result identity недопустима.");
|
|
|
|
|
}
|
|
|
|
|
const response = await fetcher(`/api/v1/laboratory/m48t/risk-quality/results/${resultId}`, {
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: { Accept: "application/json" },
|
|
|
|
|
signal,
|
|
|
|
|
});
|
|
|
|
|
if (!response.ok) throw new M48TContractError(`M4.8T недоступен: HTTP ${response.status}.`);
|
|
|
|
|
return parseResult(await response.json(), resultId);
|
|
|
|
|
}
|