feat: add local surface review triage

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 00:11:42 +03:00
parent 3449b2bc3e
commit c6c48fbc38
12 changed files with 1273 additions and 33 deletions
@@ -181,6 +181,70 @@ export interface LidarLocalSurfaceTimeline {
authority: LidarLocalSurfaceModel["authority"];
}
export type LidarLocalSurfaceReviewReason =
| "prediction-tail"
| "prediction-inlier-drop"
| "surface-height-jump"
| "surface-slope-jump"
| "surface-roughness-jump";
export interface LidarLocalSurfaceReviewItem {
rank: number;
frameIndex: number;
sourceFrameIndex: number;
sessionSeconds: number;
episodeId: string;
attention: "high" | "review";
attentionScore: number;
reasons: LidarLocalSurfaceReviewReason[];
prediction: {
available: boolean;
residualP50M: number;
residualP95M: number;
inlierFraction: number;
};
temporal: {
compared: boolean;
heightDeltaM: number;
slopeDeltaDeg: number;
roughnessDeltaM: number;
};
surface: {
sensorHeightM: number;
slopeDeg: number;
roughnessM: number;
confidence: number;
};
stepCandidatePointCount: number;
}
export interface LidarLocalSurfaceReview {
reviewProfileId: "missioncore-local-surface-attention/v1";
modelId: string;
sourcePackId: string;
sessionId: string;
available: boolean;
criteria: {
predictionTailResidualP95M: number;
predictionInlierFractionFloor: number;
surfaceHeightJumpM: number;
surfaceSlopeJumpDeg: number;
surfaceRoughnessJumpM: number;
highAttentionScore: number;
episodeMaxFrameGap: number;
};
summary: {
itemCount: number;
episodeCount: number;
highAttentionCount: number;
reviewAttentionCount: number;
reasonCounts: Record<LidarLocalSurfaceReviewReason, number>;
};
items: LidarLocalSurfaceReviewItem[];
groundTruth: false;
authority: LidarLocalSurfaceModel["authority"];
}
export class LidarLocalSurfaceContractError extends Error {}
export class LidarLocalSurfaceApiError extends Error {
@@ -200,6 +264,15 @@ const SAFE_MODEL_ID = new RegExp(`^${LOCAL_SURFACE_MODEL_PREFIX}[a-f0-9]{64}$`);
const SAFE_PACK_ID = /^e10-lidar-pack-[a-f0-9]{64}$/;
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
const SAFE_KEY = /^[a-z0-9][a-z0-9-]{0,63}$/;
const SAFE_EPISODE_ID = /^episode-[0-9]{2,4}$/;
const REVIEW_PROFILE_ID = "missioncore-local-surface-attention/v1";
const REVIEW_REASONS = [
"prediction-tail",
"prediction-inlier-drop",
"surface-height-jump",
"surface-slope-jump",
"surface-roughness-jump",
] as const satisfies readonly LidarLocalSurfaceReviewReason[];
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -925,6 +998,356 @@ export function parseLidarLocalSurfaceTimeline(
};
}
export function parseLidarLocalSurfaceReview(
value: unknown,
): LidarLocalSurfaceReview {
const source = record(value, "LiDAR local-surface review");
if (
source.schema_version !== `${LOCAL_SURFACE_SCHEMA_PREFIX}-review/v1`
|| source.review_profile_id !== REVIEW_PROFILE_ID
|| source.access !== "read-only"
|| source.ground_truth !== false
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface review несовместим",
);
}
const available = boolean(source.available, "available");
const criteriaSource = record(source.criteria, "criteria");
const criteria = {
predictionTailResidualP95M: finite(
criteriaSource.prediction_tail_residual_p95_m,
"criteria.prediction_tail_residual_p95_m",
),
predictionInlierFractionFloor: finite(
criteriaSource.prediction_inlier_fraction_floor,
"criteria.prediction_inlier_fraction_floor",
),
surfaceHeightJumpM: finite(
criteriaSource.surface_height_jump_m,
"criteria.surface_height_jump_m",
),
surfaceSlopeJumpDeg: finite(
criteriaSource.surface_slope_jump_deg,
"criteria.surface_slope_jump_deg",
),
surfaceRoughnessJumpM: finite(
criteriaSource.surface_roughness_jump_m,
"criteria.surface_roughness_jump_m",
),
highAttentionScore: finite(
criteriaSource.high_attention_score,
"criteria.high_attention_score",
),
episodeMaxFrameGap: integer(
criteriaSource.episode_max_frame_gap,
"criteria.episode_max_frame_gap",
),
};
if (
criteria.predictionTailResidualP95M <= 0
|| criteria.predictionInlierFractionFloor <= 0
|| criteria.predictionInlierFractionFloor >= 1
|| criteria.surfaceHeightJumpM <= 0
|| criteria.surfaceSlopeJumpDeg <= 0
|| criteria.surfaceRoughnessJumpM <= 0
|| criteria.highAttentionScore <= 1
|| criteria.episodeMaxFrameGap < 1
|| criteria.episodeMaxFrameGap > 100
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface review criteria несовместимы",
);
}
const summarySource = record(source.summary, "summary");
const reasonCountsSource = record(
summarySource.reason_counts,
"summary.reason_counts",
);
const reasonCounts: Record<LidarLocalSurfaceReviewReason, number> = {
"prediction-tail": integer(
reasonCountsSource["prediction-tail"],
"reason_counts.prediction-tail",
),
"prediction-inlier-drop": integer(
reasonCountsSource["prediction-inlier-drop"],
"reason_counts.prediction-inlier-drop",
),
"surface-height-jump": integer(
reasonCountsSource["surface-height-jump"],
"reason_counts.surface-height-jump",
),
"surface-slope-jump": integer(
reasonCountsSource["surface-slope-jump"],
"reason_counts.surface-slope-jump",
),
"surface-roughness-jump": integer(
reasonCountsSource["surface-roughness-jump"],
"reason_counts.surface-roughness-jump",
),
};
const summary = {
itemCount: integer(summarySource.item_count, "summary.item_count"),
episodeCount: integer(summarySource.episode_count, "summary.episode_count"),
highAttentionCount: integer(
summarySource.high_attention_count,
"summary.high_attention_count",
),
reviewAttentionCount: integer(
summarySource.review_attention_count,
"summary.review_attention_count",
),
reasonCounts,
};
const rawItems = array(source.items, "items");
if (rawItems.length > 10_000) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface review слишком большой",
);
}
const items = rawItems.map((value, index): LidarLocalSurfaceReviewItem => {
const item = record(value, `items[${index}]`);
const attention = item.attention;
if (attention !== "high" && attention !== "review") {
throw new LidarLocalSurfaceContractError(
`items[${index}].attention: несовместимое значение`,
);
}
const reasons = array(item.reasons, `items[${index}].reasons`).map(
(reason, reasonIndex): LidarLocalSurfaceReviewReason => {
if (
typeof reason !== "string"
|| !REVIEW_REASONS.includes(
reason as LidarLocalSurfaceReviewReason,
)
) {
throw new LidarLocalSurfaceContractError(
`items[${index}].reasons[${reasonIndex}]: неизвестная причина`,
);
}
return reason as LidarLocalSurfaceReviewReason;
},
);
if (!reasons.length || new Set(reasons).size !== reasons.length) {
throw new LidarLocalSurfaceContractError(
`items[${index}].reasons: несовместимый набор`,
);
}
const predictionSource = record(
item.prediction,
`items[${index}].prediction`,
);
const temporalSource = record(
item.temporal,
`items[${index}].temporal`,
);
const surfaceSource = record(item.surface, `items[${index}].surface`);
const predictionAvailable = boolean(
predictionSource.available,
`items[${index}].prediction.available`,
);
const temporalCompared = boolean(
temporalSource.compared,
`items[${index}].temporal.compared`,
);
const predictionInlierFraction = finite(
predictionSource.inlier_fraction,
`items[${index}].prediction.inlier_fraction`,
);
const predictionResidualP50M = finite(
predictionSource.residual_p50_m,
`items[${index}].prediction.residual_p50_m`,
);
const predictionResidualP95M = finite(
predictionSource.residual_p95_m,
`items[${index}].prediction.residual_p95_m`,
);
const heightDeltaM = finite(
temporalSource.height_delta_m,
`items[${index}].temporal.height_delta_m`,
);
const slopeDeltaDeg = finite(
temporalSource.slope_delta_deg,
`items[${index}].temporal.slope_delta_deg`,
);
const roughnessDeltaM = finite(
temporalSource.roughness_delta_m,
`items[${index}].temporal.roughness_delta_m`,
);
const surfaceConfidence = finite(
surfaceSource.confidence,
`items[${index}].surface.confidence`,
);
const attentionScore = finite(
item.attention_score,
`items[${index}].attention_score`,
);
const expectedReasons: LidarLocalSurfaceReviewReason[] = [];
const expectedRatios: number[] = [];
if (
predictionAvailable
&& predictionResidualP95M >= criteria.predictionTailResidualP95M
) {
expectedReasons.push("prediction-tail");
expectedRatios.push(
predictionResidualP95M / criteria.predictionTailResidualP95M,
);
}
if (
predictionAvailable
&& predictionInlierFraction < criteria.predictionInlierFractionFloor
) {
expectedReasons.push("prediction-inlier-drop");
expectedRatios.push(
(1 - predictionInlierFraction)
/ (1 - criteria.predictionInlierFractionFloor),
);
}
if (temporalCompared && heightDeltaM >= criteria.surfaceHeightJumpM) {
expectedReasons.push("surface-height-jump");
expectedRatios.push(heightDeltaM / criteria.surfaceHeightJumpM);
}
if (temporalCompared && slopeDeltaDeg >= criteria.surfaceSlopeJumpDeg) {
expectedReasons.push("surface-slope-jump");
expectedRatios.push(slopeDeltaDeg / criteria.surfaceSlopeJumpDeg);
}
if (
temporalCompared
&& roughnessDeltaM >= criteria.surfaceRoughnessJumpM
) {
expectedReasons.push("surface-roughness-jump");
expectedRatios.push(roughnessDeltaM / criteria.surfaceRoughnessJumpM);
}
const expectedAttentionScore = Math.max(...expectedRatios);
if (
predictionInlierFraction < 0
|| predictionInlierFraction > 1
|| surfaceConfidence < 0
|| surfaceConfidence > 1
|| attentionScore < 1
|| (attention === "high")
!== (attentionScore >= criteria.highAttentionScore)
|| (
reasons.some((reason) => reason.startsWith("prediction-"))
&& !predictionAvailable
)
|| (
reasons.some((reason) => reason.startsWith("surface-"))
&& !temporalCompared
)
|| reasons.join("|") !== expectedReasons.join("|")
|| !Number.isFinite(expectedAttentionScore)
|| Math.abs(attentionScore - expectedAttentionScore) > 1e-9
) {
throw new LidarLocalSurfaceContractError(
`items[${index}]: attention evidence несовместим`,
);
}
return {
rank: integer(item.rank, `items[${index}].rank`),
frameIndex: integer(item.frame_index, `items[${index}].frame_index`),
sourceFrameIndex: integer(
item.source_frame_index,
`items[${index}].source_frame_index`,
),
sessionSeconds: finite(
item.session_seconds,
`items[${index}].session_seconds`,
),
episodeId: text(
item.episode_id,
`items[${index}].episode_id`,
SAFE_EPISODE_ID,
),
attention,
attentionScore,
reasons,
prediction: {
available: predictionAvailable,
residualP50M: predictionResidualP50M,
residualP95M: predictionResidualP95M,
inlierFraction: predictionInlierFraction,
},
temporal: {
compared: temporalCompared,
heightDeltaM,
slopeDeltaDeg,
roughnessDeltaM,
},
surface: {
sensorHeightM: finite(
surfaceSource.sensor_height_m,
`items[${index}].surface.sensor_height_m`,
),
slopeDeg: finite(
surfaceSource.slope_deg,
`items[${index}].surface.slope_deg`,
),
roughnessM: finite(
surfaceSource.roughness_m,
`items[${index}].surface.roughness_m`,
),
confidence: surfaceConfidence,
},
stepCandidatePointCount: integer(
item.step_candidate_point_count,
`items[${index}].step_candidate_point_count`,
),
};
});
const observedReasonCounts: Record<LidarLocalSurfaceReviewReason, number> = {
"prediction-tail": 0,
"prediction-inlier-drop": 0,
"surface-height-jump": 0,
"surface-slope-jump": 0,
"surface-roughness-jump": 0,
};
for (const item of items) {
for (const reason of item.reasons) observedReasonCounts[reason] += 1;
}
if (
summary.itemCount !== items.length
|| summary.highAttentionCount + summary.reviewAttentionCount
!== summary.itemCount
|| summary.highAttentionCount
!== items.filter((item) => item.attention === "high").length
|| summary.episodeCount !== new Set(items.map((item) => item.episodeId)).size
|| !REVIEW_REASONS.every(
(reason) => reasonCounts[reason] === observedReasonCounts[reason],
)
|| (!available && items.length > 0)
|| items.some((item, index) =>
item.rank !== index + 1
|| (
index > 0
&& (
item.attentionScore > items[index - 1].attentionScore
|| (
item.attentionScore === items[index - 1].attentionScore
&& item.frameIndex < items[index - 1].frameIndex
)
)
)
)
) {
throw new LidarLocalSurfaceContractError(
"LiDAR local-surface review content несовместим",
);
}
return {
reviewProfileId: REVIEW_PROFILE_ID,
modelId: text(source.model_id, "model_id", SAFE_MODEL_ID),
sourcePackId: text(source.source_pack_id, "source_pack_id", SAFE_PACK_ID),
sessionId: text(source.session_id, "session_id", SAFE_ID),
available,
criteria,
summary,
items,
groundTruth: false,
authority: authority(source.authority),
};
}
async function responseJson(
response: Response,
fallback: string,
@@ -1015,3 +1438,29 @@ export async function fetchLidarLocalSurfaceTimeline(
),
);
}
export async function fetchLidarLocalSurfaceReview(
modelId: string,
options: { signal?: AbortSignal; fetcher?: LidarFetch } = {},
): Promise<LidarLocalSurfaceReview> {
if (!SAFE_MODEL_ID.test(modelId)) {
throw new LidarLocalSurfaceContractError(
"Некорректный LiDAR local-surface review",
);
}
const fetcher = options.fetcher ?? fetch;
const response = await fetcher(
`/api/v1/lidar/local-surfaces/${modelId}/review`,
{
method: "GET",
headers: { Accept: "application/json" },
signal: options.signal,
},
);
return parseLidarLocalSurfaceReview(
await responseJson(
response,
"Не удалось получить LiDAR local-surface review.",
),
);
}