feat(lab): separate evidence catalogs and record methods

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 17:56:23 +03:00
parent e9ff29297b
commit 4db00b53f7
17 changed files with 670 additions and 24 deletions
@@ -15,6 +15,8 @@ export interface E29EvidenceResult {
frameCount: number;
timelineStartSeconds: number;
timelineEndSeconds: number;
profileId: string;
producerSha256: string;
};
metrics: {
frames: {
@@ -208,6 +210,7 @@ function parseReviewFrame(value: unknown): E29ReviewFrame {
function parseEvidenceResult(value: unknown): E29EvidenceResult {
const source = record(value, "E29 result");
const identity = record(source.identity, "E29 identity");
const profile = record(identity.profile, "E29 identity.profile");
const metrics = record(source.metrics, "E29 metrics");
const frames = record(metrics.frames, "E29 metrics.frames");
const semantic = record(
@@ -253,6 +256,11 @@ function parseEvidenceResult(value: unknown): E29EvidenceResult {
identity.timeline_end_seconds,
"E29 identity.timeline_end_seconds",
),
profileId: stringValue(profile.profile_id, "E29 identity.profile.profile_id"),
producerSha256: stringValue(
identity.producer_sha256,
"E29 identity.producer_sha256",
),
},
metrics: {
frames: {
@@ -22,6 +22,12 @@ export interface LidarLocalSurfaceModel {
sessionId: string;
sourcePackId: string;
status: "diagnostic-only";
method: {
executionClass: "deterministic";
pipelineId: string;
algorithm: string;
producerSha256: string;
};
source: {
frameCount: number;
availableLidarFrames: number;
@@ -562,6 +568,8 @@ function temporalQualification(
function model(value: unknown): LidarLocalSurfaceModel {
const source = record(value, "LiDAR local-surface model");
const sourceEvidence = record(source.source, "source");
const surfaceModel = record(source.surface_model, "surface_model");
const surfaceProfile = record(surfaceModel.profile, "surface_model.profile");
const metrics = record(source.metrics, "metrics");
const frames = record(metrics.frames, "metrics.frames");
if (
@@ -611,6 +619,19 @@ function model(value: unknown): LidarLocalSurfaceModel {
sessionId: text(source.session_id, "session_id", SAFE_ID),
sourcePackId: text(source.source_pack_id, "source_pack_id", SAFE_PACK_ID),
status: "diagnostic-only",
method: {
executionClass: "deterministic",
pipelineId: text(
surfaceProfile.profile_id,
"surface_model.profile.profile_id",
),
algorithm: text(surfaceModel.kind, "surface_model.kind"),
producerSha256: text(
source.producer_sha256,
"producer_sha256",
/^[a-f0-9]{64}$/,
),
},
source: {
frameCount: integer(sourceEvidence.frame_count, "source.frame_count"),
availableLidarFrames: integer(
@@ -9,6 +9,8 @@ export type ObservationSessionStatus =
| "interrupted"
| "failed";
export type ObservationSessionScope = "all" | "source" | "laboratory";
export interface ObservationLabInstance {
labId: string;
sourceSessionId: string;
@@ -1077,15 +1079,21 @@ function requirePreparationEtag(
export async function fetchObservationSessionCatalog({
signal,
limit,
scope = "all",
fetcher = globalThis.fetch,
}: {
signal?: AbortSignal;
limit?: number;
scope?: ObservationSessionScope;
fetcher?: ObservationSessionFetch;
} = {}): Promise<ObservationSessionCatalog> {
const query = Number.isInteger(limit) && Number(limit) >= 1 && Number(limit) <= 100
? `?limit=${Number(limit)}`
: "";
const queryParameters = new URLSearchParams();
if (Number.isInteger(limit) && Number(limit) >= 1 && Number(limit) <= 100) {
queryParameters.set("limit", String(Number(limit)));
}
if (scope !== "all") queryParameters.set("scope", scope);
const serializedQuery = queryParameters.toString();
const query = serializedQuery ? `?${serializedQuery}` : "";
let response: Response;
try {
response = await fetcher(`/api/v1/observation-sessions${query}`, {
@@ -9,6 +9,7 @@ import {
type ObservationSessionFetch,
type ObservationSessionPreparation,
type ObservationSessionReplayLaunch,
type ObservationSessionScope,
type ObservationSessionSummary,
} from "./sessionArchive";
@@ -369,12 +370,14 @@ export function clearObservationReplayPreparation(
export function useObservationSessions({
limit = 100,
scope = "all",
replayEnabled = true,
onReplayBegin,
onReplayAccepted,
onReplaySettled,
}: {
limit?: number;
scope?: ObservationSessionScope;
replayEnabled?: boolean;
/** Called only after the archive is ready, immediately before replacing the old viewer. */
onReplayBegin?: (
@@ -425,7 +428,10 @@ export function useObservationSessions({
const sequence = ++catalogSequence.current;
if (foreground) setState("loading");
try {
const catalog = await fetchObservationSessionCatalog({ limit: safeLimit });
const catalog = await fetchObservationSessionCatalog({
limit: safeLimit,
scope,
});
if (!mounted.current || sequence !== catalogSequence.current) return false;
setItems(catalog.items.slice(0, safeLimit));
setState("ready");
@@ -437,7 +443,7 @@ export function useObservationSessions({
setError(errorMessage(loadError));
return false;
}
}, [safeLimit]);
}, [safeLimit, scope]);
const refresh = useCallback(() => loadCatalog(true), [loadCatalog]);