feat(lab): separate evidence catalogs and record methods
This commit is contained in:
@@ -122,6 +122,7 @@ export function ObservationSessionSelect({
|
||||
const [deleteTarget, setDeleteTarget] = useState<ObservationSessionSummary | null>(null);
|
||||
const sessions = useObservationSessions({
|
||||
limit,
|
||||
scope: "source",
|
||||
replayEnabled: blockedReason === null,
|
||||
onReplayBegin,
|
||||
onReplayAccepted,
|
||||
@@ -321,14 +322,13 @@ export function ObservationSessionArchive({
|
||||
const [deleteTarget, setDeleteTarget] = useState<ObservationSessionSummary | null>(null);
|
||||
const sessions = useObservationSessions({
|
||||
limit,
|
||||
scope: labsOnly ? "laboratory" : "source",
|
||||
replayEnabled: blockedReason === null,
|
||||
onReplayBegin,
|
||||
onReplayAccepted,
|
||||
onReplaySettled,
|
||||
});
|
||||
const items = labsOnly
|
||||
? sessions.items.filter((session) => session.lab !== null)
|
||||
: sessions.items;
|
||||
const items = sessions.items;
|
||||
|
||||
return <>
|
||||
<section
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -2466,6 +2466,7 @@
|
||||
}
|
||||
|
||||
.laboratory-task,
|
||||
.laboratory-method,
|
||||
.laboratory-result-summary {
|
||||
border-radius: 1rem;
|
||||
background: rgb(255 255 255 / 0.025);
|
||||
@@ -2473,6 +2474,7 @@
|
||||
}
|
||||
|
||||
.laboratory-task > header,
|
||||
.laboratory-method > header,
|
||||
.laboratory-result-summary > header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -2483,12 +2485,16 @@
|
||||
.laboratory-task h2,
|
||||
.laboratory-task p,
|
||||
.laboratory-task dl,
|
||||
.laboratory-method h2,
|
||||
.laboratory-method p,
|
||||
.laboratory-method ul,
|
||||
.laboratory-result-summary h2,
|
||||
.laboratory-result-summary p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.laboratory-task h2,
|
||||
.laboratory-method h2,
|
||||
.laboratory-result-summary h2 {
|
||||
margin-top: 0.3rem;
|
||||
color: var(--nodedc-text-primary);
|
||||
@@ -2497,6 +2503,7 @@
|
||||
}
|
||||
|
||||
.laboratory-task p,
|
||||
.laboratory-method p,
|
||||
.laboratory-result-summary > p {
|
||||
max-width: 66rem;
|
||||
margin-top: 0.38rem;
|
||||
@@ -2505,6 +2512,75 @@
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.laboratory-method__summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.laboratory-method__summary > div {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgb(255 255 255 / 0.035);
|
||||
padding: 0.7rem;
|
||||
}
|
||||
|
||||
.laboratory-method__summary span,
|
||||
.laboratory-method li > span,
|
||||
.laboratory-method small {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: 0.54rem;
|
||||
}
|
||||
|
||||
.laboratory-method__summary strong {
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.laboratory-method ul {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.55rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.laboratory-method li {
|
||||
display: grid;
|
||||
grid-template-columns: 5.5rem minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgb(255 255 255 / 0.025);
|
||||
padding: 0.62rem 0.7rem;
|
||||
}
|
||||
|
||||
.laboratory-method li > span {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.laboratory-method li > div {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.laboratory-method li strong {
|
||||
overflow: hidden;
|
||||
color: var(--nodedc-text-primary);
|
||||
font-size: 0.66rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.laboratory-method code {
|
||||
color: var(--nodedc-text-secondary);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.54rem;
|
||||
}
|
||||
|
||||
.laboratory-task dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
|
||||
@@ -1204,6 +1204,117 @@ interface LaboratoryOption<T extends string> {
|
||||
label: string;
|
||||
}
|
||||
|
||||
type LaboratoryExecutionClass = "deterministic" | "ai-inference" | "hybrid";
|
||||
type LaboratoryMethodCompleteness = "complete" | "legacy-partial";
|
||||
type LaboratoryEvidenceKind = "recorded-replay" | "diagnostic-model";
|
||||
|
||||
interface LaboratoryMethodComponent {
|
||||
kind: "source" | "tool" | "model" | "algorithm" | "runtime";
|
||||
name: string;
|
||||
version: string;
|
||||
role: string;
|
||||
identitySha256: string | null;
|
||||
}
|
||||
|
||||
interface LaboratoryMethod {
|
||||
completeness: LaboratoryMethodCompleteness;
|
||||
executionClass: LaboratoryExecutionClass;
|
||||
pipelineId: string;
|
||||
components: readonly LaboratoryMethodComponent[];
|
||||
}
|
||||
|
||||
function digestFromContentId(value: string | null | undefined): string | null {
|
||||
const digest = value?.split("-").at(-1) ?? "";
|
||||
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
|
||||
}
|
||||
|
||||
function publishedLaboratoryMethod(
|
||||
session: ObservationSessionSummary,
|
||||
): LaboratoryMethod {
|
||||
const method = session.lab?.provenance.method;
|
||||
if (method && typeof method === "object" && !Array.isArray(method)) {
|
||||
const value = method as Record<string, unknown>;
|
||||
const rawComponents = Array.isArray(value.components) ? value.components : [];
|
||||
const components: LaboratoryMethodComponent[] = rawComponents.flatMap((component) => {
|
||||
if (!component || typeof component !== "object" || Array.isArray(component)) return [];
|
||||
const item = component as Record<string, unknown>;
|
||||
const kind = item.kind;
|
||||
if (
|
||||
kind !== "source"
|
||||
&& kind !== "tool"
|
||||
&& kind !== "model"
|
||||
&& kind !== "algorithm"
|
||||
&& kind !== "runtime"
|
||||
) return [];
|
||||
if (
|
||||
typeof item.name !== "string"
|
||||
|| typeof item.version !== "string"
|
||||
|| typeof item.role !== "string"
|
||||
) return [];
|
||||
return [{
|
||||
kind: kind as LaboratoryMethodComponent["kind"],
|
||||
name: item.name,
|
||||
version: item.version,
|
||||
role: item.role,
|
||||
identitySha256: typeof item.identity_sha256 === "string"
|
||||
? item.identity_sha256
|
||||
: null,
|
||||
}];
|
||||
});
|
||||
const executionClass = value.execution_class;
|
||||
const completeness = value.completeness;
|
||||
if (
|
||||
components.length
|
||||
&& typeof value.pipeline_id === "string"
|
||||
&& (
|
||||
executionClass === "deterministic"
|
||||
|| executionClass === "ai-inference"
|
||||
|| executionClass === "hybrid"
|
||||
)
|
||||
&& (completeness === "complete" || completeness === "legacy-partial")
|
||||
) {
|
||||
return {
|
||||
completeness,
|
||||
executionClass,
|
||||
pipelineId: value.pipeline_id,
|
||||
components,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const resultKind = session.lab?.resultKind ?? "unknown";
|
||||
const algorithmNames: Record<string, string> = {
|
||||
"e10-integrated-perception": "Camera semantics + LiDAR metric fusion",
|
||||
"e21-realtime-envelope": "Bounded real-time perception replay",
|
||||
"e22-temporal-stability": "Temporal 2D/3D/semantic stabilization",
|
||||
"e23-inline-temporal-stability": "Inline warm-worker stabilization",
|
||||
"e24-world-motion": "World-frame motion tracking",
|
||||
"e25-persistent-support-motion": "Persistent occupied-support tracking",
|
||||
"e26-camera-ego-motion-fusion": "KB4 ego-motion + persistent LiDAR support",
|
||||
};
|
||||
return {
|
||||
completeness: "legacy-partial",
|
||||
executionClass: "hybrid",
|
||||
pipelineId: resultKind,
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: session.lab?.sourceResultId ?? session.lab?.sourceSessionId ?? session.id,
|
||||
version: "immutable source evidence",
|
||||
role: "read-only input",
|
||||
identitySha256: digestFromContentId(session.lab?.sourceResultId),
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: algorithmNames[resultKind] ?? resultKind,
|
||||
version: resultKind,
|
||||
role: "laboratory derivative",
|
||||
identitySha256: session.lab?.configSha256 ?? null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function LaboratorySelector<T extends string>({
|
||||
eyebrow,
|
||||
title,
|
||||
@@ -1285,17 +1396,20 @@ function LaboratoryTask({
|
||||
function LaboratoryEvidence({
|
||||
eyebrow,
|
||||
title,
|
||||
kind,
|
||||
resizable = false,
|
||||
children,
|
||||
}: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
kind: LaboratoryEvidenceKind;
|
||||
resizable?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
className="lab-result-surface"
|
||||
data-evidence-kind={kind}
|
||||
data-resizable={resizable ? "true" : undefined}
|
||||
>
|
||||
<header>
|
||||
@@ -1309,13 +1423,69 @@ function LaboratoryEvidence({
|
||||
);
|
||||
}
|
||||
|
||||
function LaboratoryMethodCard({ method }: { method: LaboratoryMethod }) {
|
||||
const complete = method.completeness === "complete";
|
||||
const executionLabels: Record<LaboratoryExecutionClass, string> = {
|
||||
deterministic: "Детерминированный",
|
||||
"ai-inference": "AI inference",
|
||||
hybrid: "Гибридный",
|
||||
};
|
||||
return (
|
||||
<section className="laboratory-method">
|
||||
<header>
|
||||
<div>
|
||||
<span className="section-eyebrow">МЕТОД И ВОСПРОИЗВОДИМОСТЬ</span>
|
||||
<h2>{method.pipelineId}</h2>
|
||||
<p>
|
||||
Зафиксированы вычислительный класс, инструменты, модели и алгоритмы.
|
||||
{complete
|
||||
? " Идентичности достаточны для повторного запуска."
|
||||
: " Это legacy-прогон: отсутствующие исторические версии не восстановлены задним числом."}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge tone={complete ? "success" : "warning"}>
|
||||
{complete ? "Метод полный" : "Legacy · частично"}
|
||||
</StatusBadge>
|
||||
</header>
|
||||
<div className="laboratory-method__summary">
|
||||
<div>
|
||||
<span>Класс вычисления</span>
|
||||
<strong>{executionLabels[method.executionClass]}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Компонентов</span>
|
||||
<strong>{method.components.length}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
{method.components.map((component, index) => (
|
||||
<li key={`${component.kind}:${component.name}:${index}`}>
|
||||
<span>{component.kind}</span>
|
||||
<div>
|
||||
<strong>{component.name}</strong>
|
||||
<small>{component.role} · {component.version}</small>
|
||||
</div>
|
||||
<code>
|
||||
{component.identitySha256
|
||||
? component.identitySha256.slice(0, 12)
|
||||
: "identity не зафиксирована"}
|
||||
</code>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function LaboratoryWorkTemplate({
|
||||
task,
|
||||
method,
|
||||
evidence,
|
||||
result = null,
|
||||
details = null,
|
||||
}: {
|
||||
task: ReactNode;
|
||||
method: ReactNode;
|
||||
evidence: ReactNode;
|
||||
result?: ReactNode;
|
||||
details?: ReactNode;
|
||||
@@ -1323,6 +1493,7 @@ function LaboratoryWorkTemplate({
|
||||
return (
|
||||
<div className="laboratory-work-template">
|
||||
{task}
|
||||
{method}
|
||||
{evidence}
|
||||
{result}
|
||||
{details}
|
||||
@@ -1406,10 +1577,45 @@ function E29LaboratoryResult({
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
method={(
|
||||
<LaboratoryMethodCard
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: "hybrid",
|
||||
pipelineId: result.identity.profileId,
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: result.linkedEvidence.sourceResultId,
|
||||
version: "camera-first semantic observations",
|
||||
role: "semantic identity and class",
|
||||
identitySha256: digestFromContentId(result.linkedEvidence.sourceResultId),
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: "Camera/LiDAR local-surface validation",
|
||||
version: result.identity.profileId,
|
||||
role: "range, occupied support and conflict classification",
|
||||
identitySha256: result.identity.producerSha256,
|
||||
},
|
||||
{
|
||||
kind: "model",
|
||||
name: result.linkedEvidence.localSurfaceModelId,
|
||||
version: "L2.6 local surface",
|
||||
role: "independent metric geometry",
|
||||
identitySha256: digestFromContentId(
|
||||
result.linkedEvidence.localSurfaceModelId,
|
||||
),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ИСХОДНЫЕ ДАННЫЕ"
|
||||
title="LiDAR, траектория и камера RAVNOVES00"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
{replayReady ? (
|
||||
@@ -1592,10 +1798,14 @@ function PublishedLaboratoryResult({
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
method={(
|
||||
<LaboratoryMethodCard method={publishedLaboratoryMethod(session)} />
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Исходная запись выбранной лабораторной работы"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
{replayReady ? (
|
||||
@@ -1900,10 +2110,43 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
method={(
|
||||
<LaboratoryMethodCard
|
||||
method={{
|
||||
completeness: "complete",
|
||||
executionClass: e28Model?.method.executionClass ?? "deterministic",
|
||||
pipelineId: e28Model?.method.pipelineId ?? "local-surface/unavailable",
|
||||
components: [
|
||||
{
|
||||
kind: "source",
|
||||
name: e28Model?.sourcePackId ?? "Источник не загружен",
|
||||
version: "immutable vendor MAP + pose",
|
||||
role: "read-only LiDAR evidence",
|
||||
identitySha256: digestFromContentId(e28Model?.sourcePackId),
|
||||
},
|
||||
{
|
||||
kind: "algorithm",
|
||||
name: e28Model?.method.algorithm ?? "Rolling local surface",
|
||||
version: e28Model?.method.pipelineId ?? "—",
|
||||
role: "robust local plane, occupancy and temporal residuals",
|
||||
identitySha256: e28Model?.method.producerSha256 ?? null,
|
||||
},
|
||||
{
|
||||
kind: "runtime",
|
||||
name: "Mission Core worker D",
|
||||
version: "recorded-source-paced shadow",
|
||||
role: "bounded passive replay",
|
||||
identitySha256: null,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
evidence={(
|
||||
<LaboratoryEvidence
|
||||
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||
title="Диагностическая поверхность и кадры LAB E28"
|
||||
kind="diagnostic-model"
|
||||
>
|
||||
<LidarQualityWorkspace
|
||||
embedded
|
||||
|
||||
@@ -21,6 +21,10 @@ function result(overrides = {}) {
|
||||
frame_count: 4489,
|
||||
timeline_start_seconds: 35.4,
|
||||
timeline_end_seconds: 484.0,
|
||||
profile: {
|
||||
profile_id: "camera-first-local-surface-validation/v1",
|
||||
},
|
||||
producer_sha256: "e".repeat(64),
|
||||
},
|
||||
metrics: {
|
||||
frames: {
|
||||
|
||||
@@ -57,7 +57,13 @@ function model(overrides = {}) {
|
||||
timeline_start_seconds: 1,
|
||||
timeline_end_seconds: 2,
|
||||
},
|
||||
surface_model: {},
|
||||
surface_model: {
|
||||
kind: "time-varying-rolling-local-plane",
|
||||
profile: {
|
||||
profile_id: "k1-vendor-map-dynamic-local-surface/v1",
|
||||
},
|
||||
},
|
||||
producer_sha256: "e".repeat(64),
|
||||
occupancy_policy: policy(),
|
||||
metrics: {
|
||||
frames: {
|
||||
|
||||
@@ -197,6 +197,25 @@ test("data recordings keep the compact session dropdown and laboratory results s
|
||||
assert.match(workspaceSource, /e29-camera-geometry/);
|
||||
});
|
||||
|
||||
test("source and laboratory catalogs are requested as disjoint backend projections", async () => {
|
||||
const calls = [];
|
||||
const fetcher = async (input) => {
|
||||
calls.push(String(input));
|
||||
return new Response(JSON.stringify({ items: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
await fetchObservationSessionCatalog({ limit: 100, scope: "source", fetcher });
|
||||
await fetchObservationSessionCatalog({ limit: 100, scope: "laboratory", fetcher });
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"/api/v1/observation-sessions?limit=100&scope=source",
|
||||
"/api/v1/observation-sessions?limit=100&scope=laboratory",
|
||||
]);
|
||||
});
|
||||
|
||||
test("session catalog exposes authoritative background preparation state", () => {
|
||||
const catalog = decodeObservationSessionCatalog({
|
||||
items: [session({
|
||||
|
||||
@@ -45,6 +45,9 @@ test("every laboratory result uses the shared evidence template", async () => {
|
||||
|
||||
assert.match(source, /function LaboratoryWorkTemplate\(/);
|
||||
assert.match(source, /function LaboratoryEvidence\(/);
|
||||
assert.match(source, /function LaboratoryMethodCard\(/);
|
||||
assert.match(source, /method:\s*ReactNode/);
|
||||
assert.match(source, /data-evidence-kind=\{kind\}/);
|
||||
assert.match(source, /data-viewer-focused=/);
|
||||
assert.match(css, /height:\s*clamp\(42rem,\s*68vh,\s*58rem\)/);
|
||||
assert.match(css, /resize:\s*vertical/);
|
||||
|
||||
Reference in New Issue
Block a user