feat(polygon): qualify GOOSE ground providers

This commit is contained in:
DCCONSTRUCTIONS
2026-07-25 16:02:20 +03:00
parent 60ba64004b
commit d6decbe05c
14 changed files with 2718 additions and 22 deletions
@@ -0,0 +1,393 @@
export interface QualificationMetricAggregate {
micro: {
groundIou: number;
naturalGroundRecall: number;
obstacleNonGroundRecall: number;
};
latencyMs: {
p50: number;
p95: number;
maximum: number;
};
assignedFraction: number;
sourceCoverage: number;
frameCount: number;
}
export interface QualificationCheck {
checkId: string;
observed: number;
operator: ">=" | "<=";
threshold: number;
passed: boolean;
}
export interface QualificationWorstFrame {
frameId: string;
currentGroundIou: number;
patchworkGroundIou: number;
groundIouDelta: number;
patchworkNaturalGroundRecall: number;
}
export interface GroundQualification {
runId: string;
identitySha256: string;
sourceId: string;
split: string;
frameCount: number;
current: QualificationMetricAggregate;
patchwork: QualificationMetricAggregate;
degradations: Record<string, QualificationMetricAggregate>;
checks: QualificationCheck[];
worstFrames: QualificationWorstFrame[];
decision: {
status: "shadow-candidate" | "qualification-rejected";
passed: boolean;
promotedToNavigationOrSafety: false;
reason: string;
};
}
export interface GroundFailurePreview {
runId: string;
frameId: string;
sourcePointCount: number;
pointCount: number;
pointsXyzM: Array<[number, number, number]>;
groundTruthGround: number[];
evaluated: number[];
currentGround: number[];
patchworkGround: number[];
currentDisagreement: number[];
patchworkDisagreement: number[];
}
export class GroundQualificationContractError extends Error {}
export class GroundQualificationApiError extends Error {
constructor(message: string, readonly status: number | null = null) {
super(message);
}
}
type QualificationFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SHA256 = /^[a-f0-9]{64}$/;
function record(value: unknown, field: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new GroundQualificationContractError(`${field} должен быть объектом.`);
}
return value as Record<string, unknown>;
}
function array(value: unknown, field: string, maximum: number): unknown[] {
if (!Array.isArray(value) || value.length > maximum) {
throw new GroundQualificationContractError(`${field} должен быть ограниченным массивом.`);
}
return value;
}
function text(value: unknown, field: string, maximum = 256): string {
if (typeof value !== "string" || !value.trim() || value.length > maximum) {
throw new GroundQualificationContractError(`${field} должен быть непустой строкой.`);
}
return value;
}
function id(value: unknown, field: string): string {
const result = text(value, field, 128);
if (!SAFE_ID.test(result)) {
throw new GroundQualificationContractError(`${field} содержит недопустимый идентификатор.`);
}
return result;
}
function number(value: unknown, field: string, minimum = 0): number {
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum) {
throw new GroundQualificationContractError(`${field} должен быть конечным числом.`);
}
return value;
}
function fraction(value: unknown, field: string): number {
const result = number(value, field);
if (result > 1) {
throw new GroundQualificationContractError(`${field} должен быть долей 0..1.`);
}
return result;
}
function boolean(value: unknown, field: string): boolean {
if (typeof value !== "boolean") {
throw new GroundQualificationContractError(`${field} должен быть boolean.`);
}
return value;
}
function aggregate(value: unknown, field: string): QualificationMetricAggregate {
const source = record(value, field);
const micro = record(source.micro, `${field}.micro`);
const latency = record(source.latency_ms, `${field}.latency_ms`);
return {
micro: {
groundIou: fraction(micro.ground_iou, `${field}.micro.ground_iou`),
naturalGroundRecall: fraction(
micro.natural_ground_recall,
`${field}.micro.natural_ground_recall`,
),
obstacleNonGroundRecall: fraction(
micro.obstacle_non_ground_recall,
`${field}.micro.obstacle_non_ground_recall`,
),
},
latencyMs: {
p50: number(latency.p50, `${field}.latency_ms.p50`),
p95: number(latency.p95, `${field}.latency_ms.p95`),
maximum: number(latency.maximum, `${field}.latency_ms.maximum`),
},
assignedFraction: fraction(source.assigned_fraction, `${field}.assigned_fraction`),
sourceCoverage: fraction(source.source_coverage, `${field}.source_coverage`),
frameCount: number(source.frame_count, `${field}.frame_count`, 1),
};
}
export function decodeGroundQualification(payload: unknown): GroundQualification {
const source = record(payload, "qualification");
if (
source.schema_version !== "missioncore.polygon-ground-qualification/v1"
|| source.access !== "read-only"
) {
throw new GroundQualificationContractError("Неизвестная схема квалификации.");
}
const runId = id(source.run_id, "run_id");
const identitySha256 = text(source.identity_sha256, "identity_sha256", 64);
if (!SHA256.test(identitySha256)) {
throw new GroundQualificationContractError("identity_sha256 должен содержать SHA-256.");
}
const aggregates = record(source.aggregates, "aggregates");
const degradationSource = record(source.degradations, "degradations");
const degradations: Record<string, QualificationMetricAggregate> = {};
Object.entries(degradationSource).forEach(([profileId, value]) => {
if (!SAFE_ID.test(profileId) || Object.keys(degradations).length >= 32) {
throw new GroundQualificationContractError("Профили деградации имеют неверный контракт.");
}
degradations[profileId] = aggregate(value, `degradations.${profileId}`);
});
const checks = array(source.checks, "checks", 128).map((value, index) => {
const check = record(value, `checks[${index}]`);
const rawOperator = text(check.operator, `checks[${index}].operator`, 2);
if (rawOperator !== ">=" && rawOperator !== "<=") {
throw new GroundQualificationContractError("Проверка содержит неизвестный оператор.");
}
const operator: QualificationCheck["operator"] = rawOperator;
return {
checkId: id(check.check_id, `checks[${index}].check_id`),
observed: number(check.observed, `checks[${index}].observed`, -Infinity),
operator,
threshold: number(check.threshold, `checks[${index}].threshold`, -Infinity),
passed: boolean(check.passed, `checks[${index}].passed`),
};
});
const worstFrames = array(source.worst_frames, "worst_frames", 20).map(
(value, index) => {
const frame = record(value, `worst_frames[${index}]`);
return {
frameId: id(frame.frame_id, `worst_frames[${index}].frame_id`),
currentGroundIou: fraction(
frame.current_ground_iou,
`worst_frames[${index}].current_ground_iou`,
),
patchworkGroundIou: fraction(
frame.patchwork_ground_iou,
`worst_frames[${index}].patchwork_ground_iou`,
),
groundIouDelta: number(
frame.ground_iou_delta,
`worst_frames[${index}].ground_iou_delta`,
-1,
),
patchworkNaturalGroundRecall: fraction(
frame.patchwork_natural_ground_recall,
`worst_frames[${index}].patchwork_natural_ground_recall`,
),
};
},
);
const decision = record(source.decision, "decision");
const status = text(decision.status, "decision.status", 64);
if (status !== "shadow-candidate" && status !== "qualification-rejected") {
throw new GroundQualificationContractError("Решение квалификации неизвестно.");
}
if (decision.promoted_to_navigation_or_safety !== false) {
throw new GroundQualificationContractError(
"Квалификация не может принимать навигацию или safety.",
);
}
const frameCount = number(source.frame_count, "frame_count", 1);
const current = aggregate(aggregates.current, "aggregates.current");
const patchwork = aggregate(aggregates.patchworkpp, "aggregates.patchworkpp");
if (current.frameCount !== frameCount || patchwork.frameCount !== frameCount) {
throw new GroundQualificationContractError("Агрегаты не совпадают с числом кадров.");
}
return {
runId,
identitySha256,
sourceId: text(source.source_id, "source_id"),
split: id(source.split, "split"),
frameCount,
current,
patchwork,
degradations,
checks,
worstFrames,
decision: {
status,
passed: boolean(decision.passed, "decision.passed"),
promotedToNavigationOrSafety: false,
reason: text(decision.reason, "decision.reason", 512),
},
};
}
function mask(value: unknown, field: string, expected: number): number[] {
const values = array(value, field, 50_000).map((item, index) => {
if (item !== 0 && item !== 1) {
throw new GroundQualificationContractError(`${field}[${index}] должен быть 0 или 1.`);
}
return item;
});
if (values.length !== expected) {
throw new GroundQualificationContractError(`${field} не совпадает с point_count.`);
}
return values as number[];
}
export function decodeGroundFailurePreview(payload: unknown): GroundFailurePreview {
const source = record(payload, "failure preview");
if (
source.schema_version !== "missioncore.polygon-ground-failure-preview/v1"
|| source.access !== "read-only"
) {
throw new GroundQualificationContractError("Неизвестная схема preview.");
}
const pointCount = number(source.point_count, "point_count", 1);
const pointsXyzM = array(source.points_xyz_m, "points_xyz_m", 50_000).map(
(value, index): [number, number, number] => {
const tuple = array(value, `points_xyz_m[${index}]`, 3);
if (tuple.length !== 3) {
throw new GroundQualificationContractError("Точка должна содержать XYZ.");
}
return [
number(tuple[0], `points_xyz_m[${index}].x`, -Infinity),
number(tuple[1], `points_xyz_m[${index}].y`, -Infinity),
number(tuple[2], `points_xyz_m[${index}].z`, -Infinity),
];
},
);
if (pointsXyzM.length !== pointCount) {
throw new GroundQualificationContractError("points_xyz_m не совпадает с point_count.");
}
return {
runId: id(source.run_id, "run_id"),
frameId: id(source.frame_id, "frame_id"),
sourcePointCount: number(source.source_point_count, "source_point_count", pointCount),
pointCount,
pointsXyzM,
groundTruthGround: mask(source.ground_truth_ground, "ground_truth_ground", pointCount),
evaluated: mask(source.evaluated, "evaluated", pointCount),
currentGround: mask(source.current_ground, "current_ground", pointCount),
patchworkGround: mask(source.patchwork_ground, "patchwork_ground", pointCount),
currentDisagreement: mask(
source.current_disagreement,
"current_disagreement",
pointCount,
),
patchworkDisagreement: mask(
source.patchwork_disagreement,
"patchwork_disagreement",
pointCount,
),
};
}
async function requestJson(
url: string,
signal: AbortSignal | undefined,
fetcher: QualificationFetch,
): Promise<unknown> {
let response: Response;
try {
response = await fetcher(url, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") throw error;
throw new GroundQualificationApiError("Не удалось загрузить квалификацию.");
}
let body: unknown;
try {
body = await response.json();
} catch {
throw new GroundQualificationContractError("Polygon API вернул повреждённый JSON.");
}
if (!response.ok) {
const detail = typeof body === "object" && body !== null && "detail" in body
? String((body as { detail: unknown }).detail)
: `Polygon API HTTP ${response.status}.`;
throw new GroundQualificationApiError(detail, response.status);
}
return body;
}
export async function fetchGroundQualification(
runId: string,
{
signal,
fetcher = globalThis.fetch,
}: { signal?: AbortSignal; fetcher?: QualificationFetch } = {},
): Promise<GroundQualification | null> {
if (!SAFE_ID.test(runId)) {
throw new GroundQualificationContractError("Недопустимый run_id.");
}
try {
return decodeGroundQualification(
await requestJson(
`/api/v1/polygon/runs/${encodeURIComponent(runId)}/qualification`,
signal,
fetcher,
),
);
} catch (error) {
if (error instanceof GroundQualificationApiError && error.status === 404) return null;
throw error;
}
}
export async function fetchGroundFailurePreview(
runId: string,
frameId: string,
{
signal,
fetcher = globalThis.fetch,
}: { signal?: AbortSignal; fetcher?: QualificationFetch } = {},
): Promise<GroundFailurePreview> {
if (!SAFE_ID.test(runId) || !SAFE_ID.test(frameId)) {
throw new GroundQualificationContractError("Недопустимый идентификатор preview.");
}
return decodeGroundFailurePreview(
await requestJson(
`/api/v1/polygon/runs/${encodeURIComponent(runId)}/qualification/failures/`
+ encodeURIComponent(frameId),
signal,
fetcher,
),
);
}