refactor(lab): canonize selected evidence reports

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 11:49:44 +03:00
parent 4c763bd8aa
commit de12e96297
30 changed files with 2876 additions and 134 deletions
@@ -29,6 +29,11 @@ export interface LaboratoryAnnotationAction {
onClick: () => void;
}
export interface LaboratoryViewAction {
label: string;
onClick: () => void;
}
export interface WorkspaceRendererProps {
definition: WorkspaceDefinition;
state: MissionRuntimeState | null;
@@ -65,4 +70,5 @@ export interface WorkspaceRendererProps {
onLaboratoryAnnotationActionChange: (
action: LaboratoryAnnotationAction | null,
) => void;
onLaboratoryViewActionChange: (action: LaboratoryViewAction | null) => void;
}
@@ -11,8 +11,6 @@ import {
LaboratorySelector,
LaboratorySummary,
LaboratoryWorkTemplate,
type LaboratoryMethod,
type LaboratoryMethodComponent,
} from "../../components/laboratory/LaboratoryPresentation";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
import { useObservationSessions } from "../../core/observation/useObservationSessions";
@@ -26,9 +24,7 @@ import {
fetchE30ReviewCatalog,
type E30ReviewResult,
} from "../../core/laboratory/e30Review";
import {
type AdvancedLaboratoryResults,
} from "../../core/laboratory/advancedResults";
import { type AdvancedLaboratoryResults } from "../../core/laboratory/advancedResults";
import {
fetchLidarLocalSurfaces,
type LidarLocalSurfaceModel,
@@ -42,11 +38,15 @@ import {
advancedLaboratorySourceSession,
isAdvancedLaboratoryWorkId,
} from "./AdvancedLaboratoryResult";
import { LaboratoryEvidenceReportView } from "./LaboratoryEvidenceReportView";
import {
e28LaboratoryBrief, e29LaboratoryBrief,
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
} from "./laboratoryArchiveBriefs";
import { useAdvancedLaboratoryCatalog } from "./useAdvancedLaboratoryCatalog";
import { useLaboratoryValueReviewIndex } from "./useLaboratoryValueReviewIndex";
import { useLaboratoryEvidenceReport } from "./useLaboratoryEvidenceReport";
import { useLaboratoryViewMode } from "./useLaboratoryViewMode";
import { useL34AnnotationCapability } from "./annotation/useL34AnnotationCapability";
import {
buildLaboratoryCatalog,
@@ -54,6 +54,16 @@ import {
experimentOptionsForProfile,
workOptionsForExperiment,
} from "./laboratoryArchiveProfiles";
import {
experimentOptionsWithSignals,
profileOptionsWithSignals,
projectLaboratoryValueReviews,
workOptionsWithSignals,
} from "./laboratoryValueReviewProjection";
import {
digestFromContentId,
publishedLaboratoryMethod,
} from "./publishedLaboratoryMethod";
import type {
LaboratoryCatalogSeed,
LaboratoryExperimentId,
@@ -64,98 +74,6 @@ type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
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 formatSeconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`;
}
@@ -550,6 +468,10 @@ function PublishedLaboratoryResult({
}
export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const [viewMode] = useLaboratoryViewMode(
props.onLaboratoryViewActionChange,
);
const laboratoryValueReview = useLaboratoryValueReviewIndex();
const [profileId, setProfileId] = useState<LaboratoryProfileId>(
"rig-right-yolox-lidar-range-v1",
);
@@ -602,7 +524,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
},
});
const advancedResults: AdvancedLaboratoryResults = advanced.results;
const annotationWorkspace = useL34AnnotationCapability({ selectedWorkId: workId, l34Result: advancedResults.l34, l34dResult: advancedResults.l34d, l34eResult: advancedResults.l34e, e46Result: advancedResults.e46, e46aResult: advancedResults.e46a, onActionChange: props.onLaboratoryAnnotationActionChange });
const annotationWorkspace = useL34AnnotationCapability({ selectedWorkId: viewMode === "laboratory" ? workId : "", l34Result: advancedResults.l34, l34dResult: advancedResults.l34d, l34eResult: advancedResults.l34e, e46Result: advancedResults.e46, e46aResult: advancedResults.e46a, onActionChange: props.onLaboratoryAnnotationActionChange });
useEffect(() => {
const controller = new AbortController();
setEvidenceLoading(true);
@@ -651,6 +573,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
items.push({
id: "e28-local-surface",
createdAtUtc: e28Model.createdAtUtc ?? "",
evidenceId: e28Model.modelId,
});
}
if (
@@ -660,12 +583,14 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
items.push({
id: "e29-camera-geometry",
createdAtUtc: e29Result.createdAtUtc ?? "",
evidenceId: e29Result.resultId,
});
}
if (e30Result && sourceSessions.has(e30Result.sourceSessionId)) {
items.push({
id: "e30-evidence-review",
createdAtUtc: e30Result.createdAtUtc ?? "",
evidenceId: e30Result.resultId,
});
}
return items.filter(({ createdAtUtc }) => createdAtUtc.trim());
@@ -684,18 +609,33 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
}),
[advanced.index, knownWorks, publishedWorks, rigLabel],
);
const valueReviews = useMemo(() => projectLaboratoryValueReviews({
catalog,
index: laboratoryValueReview.index,
publishedWorks,
}), [catalog, laboratoryValueReview.index, publishedWorks]);
const profiles = useMemo(
() => buildLaboratoryProfiles(catalog),
[catalog],
() => profileOptionsWithSignals(buildLaboratoryProfiles(catalog), catalog, valueReviews),
[catalog, valueReviews],
);
const experimentOptions = useMemo(
() => experimentOptionsForProfile(profileId, catalog),
[catalog, profileId],
() => experimentOptionsWithSignals(
experimentOptionsForProfile(profileId, catalog), profileId, catalog, valueReviews,
),
[catalog, profileId, valueReviews],
);
const workOptions = useMemo(
() => workOptionsForExperiment(profileId, experimentId, catalog),
[catalog, experimentId, profileId],
() => workOptionsWithSignals(
workOptionsForExperiment(profileId, experimentId, catalog), valueReviews,
),
[catalog, experimentId, profileId, valueReviews],
);
const selectedCatalog = catalog.find((entry) => entry.id === workId) ?? null;
const evidenceReport = useLaboratoryEvidenceReport({
workId,
resultId: selectedCatalog?.evidenceId ?? "",
enabled: viewMode === "report" && selectedCatalog !== null,
});
const selectedSessionId = workId.startsWith("session:")
? workId.slice("session:".length)
: null;
@@ -828,6 +768,17 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
|| props.observationLayout.maximizedFloatingSourceId,
);
if (viewMode === "report" && selectedCatalog) {
return (
<LaboratoryEvidenceReportView
catalog={selectedCatalog}
report={evidenceReport.report}
loading={evidenceReport.loading}
error={evidenceReport.error}
/>
);
}
return (
<div
className="lab-archive-workspace"
@@ -0,0 +1,288 @@
import { Icon, StatusBadge } from "@nodedc/ui-react";
import type {
JsonValue,
LaboratoryEvidenceReport,
} from "../../core/laboratory/evidenceReport";
import {
laboratoryTimestamp,
type LaboratoryCatalogEntry,
} from "./laboratoryArchiveProfiles";
const FIELD_LABELS: Readonly<Record<string, string>> = {
acceptance: "Приёмка",
accepted: "Принято",
architecture: "Архитектура",
authority: "Полномочия",
byte_length: "Размер",
calibration_model: "Модель калибровки",
calibration_sha256: "SHA калибровки",
camera_source_id: "Камера",
checks: "Проверки",
commands_enabled: "Команды разрешены",
completeness: "Полнота",
config_sha256: "SHA конфига",
container_image: "Образ контейнера",
core_capacity_fps: "Вычислительная ёмкость, FPS",
core_path_p95_ms: "Core path p95, мс",
created_at_utc: "Создано UTC",
decision: "Решение",
detector: "Детектор",
execution_class: "Класс исполнения",
frame_count: "Кадры",
failed_frame_count: "Ошибки кадров",
gpu_memory_used_mib: "GPU memory, MiB",
gpu_name: "GPU",
gpu_power_watts: "GPU power, W",
gpu_temperature_celsius: "GPU temperature, °C",
gpu_utilization_percent: "GPU utilization, %",
ground_truth: "Ground truth",
identity_sha256: "SHA identity",
limitations: "Ограничения",
metrics: "Метрики",
model_sha256: "SHA модели",
navigation_or_safety_accepted: "Допуск navigation/safety",
next_action: "Следующее действие",
pipeline_id: "Pipeline",
preprocessing_contract: "Preprocessing contract",
profile_sha256: "SHA профиля",
provider_promoted: "Provider promoted",
report_sha256: "SHA отчёта",
resolution: "Разрешение",
resources: "Ресурсы",
result_id: "Result identity",
runtime: "Runtime",
schema_version: "Версия схемы",
session_id: "Сессия",
source: "Источник",
configuration: "Конфигурация",
status: "Статус",
stream_sha256: "SHA потока",
worker_host: "Worker host",
};
const COMPLETENESS_LABELS: Readonly<Record<string, string>> = {
identity: "Identity",
source: "Источник",
method: "Метод и модули",
execution: "Runtime / worker",
resources: "Нагрузка",
metrics: "Метрики",
gates: "Acceptance gates",
decision: "Решение",
limitations: "Ограничения",
authority: "Полномочия",
artifacts: "Артефакты",
visual_evidence: "Визуал",
};
function fieldLabel(value: string): string {
return FIELD_LABELS[value] ?? value.replaceAll("_", " ");
}
function primitive(value: string | number | boolean | null): string {
if (value === null) return "Не зафиксировано";
if (typeof value === "boolean") return value ? "Да" : "Нет";
if (typeof value === "number") {
return value.toLocaleString("ru-RU", { maximumFractionDigits: 6 });
}
return value;
}
function EvidenceValue({ value, depth = 0 }: { value: JsonValue; depth?: number }) {
if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
return <span className="laboratory-evidence-report__value">{primitive(value as string | number | boolean | null)}</span>;
}
if (Array.isArray(value)) {
if (!value.length) return <span className="laboratory-evidence-report__missing">Пустой список</span>;
return (
<ol className="laboratory-evidence-report__array">
{value.map((item, index) => (
<li key={index}><EvidenceValue value={item} depth={depth + 1} /></li>
))}
</ol>
);
}
return (
<dl className="laboratory-evidence-report__tree" data-depth={depth}>
{Object.entries(value).map(([key, item]) => (
<div key={key}>
<dt>{fieldLabel(key)}</dt>
<dd><EvidenceValue value={item} depth={depth + 1} /></dd>
</div>
))}
</dl>
);
}
function ReportSection({
eyebrow,
title,
value,
}: {
eyebrow: string;
title: string;
value: JsonValue | undefined;
}) {
return (
<section className="laboratory-evidence-report__section">
<header>
<span className="section-eyebrow">{eyebrow}</span>
<h3>{title}</h3>
</header>
{value === null || value === undefined ? (
<p className="laboratory-evidence-report__missing">
Не зафиксировано в immutable evidence этой лабораторной работы.
</p>
) : (
<EvidenceValue value={value} />
)}
</section>
);
}
function LoadingReport({ catalog }: { catalog: LaboratoryCatalogEntry }) {
return (
<div className="laboratory-result-pending" role="status">
<span className="busy-indicator" aria-hidden="true" />
<strong>Проверяем доказательства {catalog.variantName}</strong>
<p>Сверяем identity, manifest и SHA-256 каждого опубликованного артефакта.</p>
</div>
);
}
export function LaboratoryEvidenceReportView({
catalog,
report,
loading,
error,
}: {
catalog: LaboratoryCatalogEntry;
report: LaboratoryEvidenceReport | null;
loading: boolean;
error: string | null;
}) {
if (loading) return <LoadingReport catalog={catalog} />;
if (!report) {
return (
<section className="laboratory-evidence-report laboratory-evidence-report--unavailable">
<header className="laboratory-evidence-report__header">
<div>
<span className="section-eyebrow">ОТЧЁТ ВЫБРАННОЙ LAB · EVIDENCE IDENTITY</span>
<h2>{catalog.variantName}</h2>
<p>{catalog.evidenceId}</p>
</div>
<StatusBadge tone="warning">Неполный evidence contract</StatusBadge>
</header>
<div className="laboratory-evidence-report__notice">
<Icon name="database" size={20} />
<div>
<strong>Канонический доказательный JSON не опубликован</strong>
<p>{error ?? "Для этой legacy LAB доступен визуал, но нет полного manifest/report контракта."}</p>
</div>
</div>
<dl className="laboratory-evidence-report__identity">
<div><dt>LAB</dt><dd>{catalog.id}</dd></div>
<div><dt>Evidence identity</dt><dd>{catalog.evidenceId}</dd></div>
<div><dt>Дата</dt><dd>{laboratoryTimestamp(catalog.createdAtUtc)}</dd></div>
</dl>
</section>
);
}
const recorded = Object.values(report.completeness).filter((value) => value === "recorded").length;
const total = Object.keys(report.completeness).length;
return (
<div className="laboratory-evidence-report">
<header className="laboratory-evidence-report__header">
<div>
<span className="section-eyebrow">ОТЧЁТ ВЫБРАННОЙ LAB · IMMUTABLE EVIDENCE</span>
<h2>{catalog.variantName}</h2>
<p>{catalog.profileName} · {laboratoryTimestamp(catalog.createdAtUtc)}</p>
</div>
<StatusBadge tone={recorded === total ? "success" : "warning"}>
{recorded}/{total} доказательных разделов
</StatusBadge>
</header>
<section className="laboratory-evidence-report__integrity">
<header>
<div>
<span className="section-eyebrow">ЦЕЛОСТНОСТЬ И ПРОИСХОЖДЕНИЕ</span>
<h3>Отчёт собран из проверенного manifest, а не из UI-копирайта</h3>
</div>
<StatusBadge tone={report.proof.artifactCount === report.proof.verifiedArtifactCount ? "success" : "warning"}>
SHA-256 {report.proof.verifiedArtifactCount}/{report.proof.artifactCount}
</StatusBadge>
</header>
<dl className="laboratory-evidence-report__identity">
<div><dt>LAB work ID</dt><dd>{report.workId}</dd></div>
<div><dt>Result identity</dt><dd>{report.resultId}</dd></div>
<div><dt>Identity SHA-256</dt><dd>{report.proof.identitySha256}</dd></div>
<div><dt>Report SHA-256</dt><dd>{report.proof.reportSha256 ?? "Отдельный report artifact не зафиксирован"}</dd></div>
<div><dt>Manifest/document SHA-256</dt><dd>{report.proof.documentSha256}</dd></div>
<div><dt>Schema</dt><dd>{report.proof.reportSchemaVersion ?? report.proof.documentSchemaVersion}</dd></div>
</dl>
<dl className="laboratory-evidence-report__completeness">
{Object.entries(report.completeness).map(([key, state]) => (
<div key={key} data-state={state}>
<dt>{COMPLETENESS_LABELS[key] ?? fieldLabel(key)}</dt>
<dd>{state === "recorded" ? "Зафиксировано" : "Не зафиксировано"}</dd>
</div>
))}
</dl>
</section>
<div className="laboratory-evidence-report__grid">
<ReportSection eyebrow="SOURCE CONTRACT" title="Источник, калибровка и preprocessing" value={report.source} />
<ReportSection eyebrow="RUN CONFIGURATION" title="Профиль, параметры и пороги запуска" value={report.configuration} />
<ReportSection eyebrow="METHOD CONTRACT" title="Модули, модели, алгоритмы и их identity" value={report.method} />
<ReportSection eyebrow="EXECUTION" title="Worker, runtime и фактическое исполнение" value={report.execution} />
<ReportSection eyebrow="RESOURCE TELEMETRY" title="Нагрузка CPU / RAM / GPU" value={report.resources} />
<ReportSection eyebrow="MEASUREMENTS" title="Измеренные показатели" value={report.metrics} />
<ReportSection eyebrow="ACCEPTANCE" title="Пороги, проверки и результат gate" value={report.gates} />
<ReportSection eyebrow="DECISION" title="Решение, границы вывода и следующий шаг" value={report.decision} />
<ReportSection eyebrow="LIMITATIONS" title="Что эта LAB не доказывает" value={report.limitations} />
<ReportSection eyebrow="AUTHORITY" title="Сохранённые запреты и полномочия" value={report.authority} />
<ReportSection
eyebrow="VISUAL EVIDENCE"
title="Визуальная проверка и связанные артефакты"
value={report.completeness.visual_evidence === "recorded" ? report.visualEvidence : undefined}
/>
</div>
<section className="laboratory-evidence-report__section laboratory-evidence-report__artifacts">
<header>
<span className="section-eyebrow">VERIFIED ARTIFACTS</span>
<h3>Файлы доказательства, размер и полный SHA-256</h3>
</header>
{report.artifacts.length ? (
<div className="laboratory-evidence-report__artifact-list">
{report.artifacts.map((artifact) => (
<article key={artifact.path}>
<header>
<strong>{artifact.kind ?? "artifact"}</strong>
<StatusBadge tone="success">SHA verified</StatusBadge>
</header>
<p>{artifact.path}</p>
<dl>
<div><dt>Размер</dt><dd>{artifact.byteLength.toLocaleString("ru-RU")} байт</dd></div>
<div><dt>SHA-256</dt><dd>{artifact.sha256}</dd></div>
<div><dt>Schema / media</dt><dd>{artifact.schemaVersion ?? artifact.mediaType ?? "Не размечено"}</dd></div>
</dl>
</article>
))}
</div>
) : <p className="laboratory-evidence-report__missing">Artifact manifest не зафиксирован.</p>}
</section>
<section className="laboratory-evidence-report__section laboratory-evidence-report__canonical-json">
<header>
<span className="section-eyebrow">CANONICAL JSON · READ-ONLY</span>
<h3>Полный нормализованный evidence-report без потери исходных полей</h3>
</header>
<pre>{JSON.stringify(report.canonicalJson, null, 2)}</pre>
</section>
</div>
);
}
@@ -31,11 +31,13 @@ export type LaboratoryWorkId =
export interface LaboratoryCatalogSeed {
id: LaboratoryWorkId;
createdAtUtc: string;
evidenceId: string;
}
export interface LaboratoryCatalogEntry {
id: LaboratoryWorkId;
createdAtUtc: string;
evidenceId: string;
profileId: LaboratoryProfileId;
profileName: string;
experimentId: LaboratoryExperimentId;
@@ -369,18 +371,23 @@ export function buildLaboratoryCatalog({
advancedIndex: readonly AdvancedLaboratoryIndexItem[];
publishedWorks: readonly ObservationSessionSummary[];
}): readonly LaboratoryCatalogEntry[] {
const seeded = new Map<LaboratoryWorkId, string>();
for (const work of knownWorks) seeded.set(work.id, work.createdAtUtc);
for (const work of advancedIndex) seeded.set(work.workId, work.createdAtUtc);
const seeded = new Map<LaboratoryWorkId, { createdAtUtc: string; evidenceId: string }>();
for (const work of knownWorks) {
seeded.set(work.id, { createdAtUtc: work.createdAtUtc, evidenceId: work.evidenceId });
}
for (const work of advancedIndex) {
seeded.set(work.workId, { createdAtUtc: work.createdAtUtc, evidenceId: work.resultId });
}
const entries: LaboratoryCatalogEntry[] = [];
for (const [id, createdAtUtc] of seeded) {
for (const [id, identity] of seeded) {
if (id.startsWith("session:")) continue;
const definition = KNOWN_WORKS[id as Exclude<LaboratoryWorkId, `session:${string}`>];
if (!definition) continue;
entries.push({
id,
createdAtUtc,
createdAtUtc: identity.createdAtUtc,
evidenceId: identity.evidenceId,
profileId: definition.profileId,
profileName: definition.profileName(rigLabel),
experimentId: definition.experimentId,
@@ -396,6 +403,7 @@ export function buildLaboratoryCatalog({
entries.push({
id: `session:${session.id}`,
createdAtUtc: session.lab?.runCreatedAtUtc ?? session.startedAtUtc,
evidenceId: session.lab?.sourceResultId ?? session.lab?.resultId ?? session.id,
profileId,
profileName: `${rig(rigLabel)} RIGHT · ${pipelineName}`,
experimentId: `${profileId}:ravnoves00`,
@@ -0,0 +1,125 @@
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
import type {
LaboratoryValueReviewEntry,
LaboratoryValueReviewIndex,
LaboratoryValueLifecycle,
LaboratoryValueSignal,
LaboratoryVisualEvidence,
} from "../../core/laboratory/valueReviewIndex";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
import type {
LaboratoryCatalogEntry,
LaboratoryExperimentId,
LaboratoryProfileId,
LaboratoryWorkId,
} from "./laboratoryArchiveProfiles";
export type ProjectedLaboratorySignal = LaboratoryValueSignal | "unreviewed";
export interface ProjectedLaboratoryValueReview {
catalog: LaboratoryCatalogEntry;
signal: ProjectedLaboratorySignal;
lifecycle: LaboratoryValueLifecycle;
visualEvidence: LaboratoryVisualEvidence;
}
function legacySessionReview(
catalog: LaboratoryCatalogEntry,
session: ObservationSessionSummary,
): ProjectedLaboratoryValueReview {
const passed = session.lab?.provenance.benchmark_passed;
const signal: LaboratoryValueSignal = passed === true
? "progress"
: passed === false
? "failed"
: "retained";
return {
catalog,
signal,
lifecycle: "legacy",
visualEvidence: "available",
};
}
function reviewedValue(
catalog: LaboratoryCatalogEntry,
review: LaboratoryValueReviewEntry,
): ProjectedLaboratoryValueReview {
return {
catalog,
signal: review.signal,
lifecycle: review.lifecycle,
visualEvidence: review.visualEvidence,
};
}
export function projectLaboratoryValueReviews({
catalog,
index,
publishedWorks,
}: {
catalog: readonly LaboratoryCatalogEntry[];
index: LaboratoryValueReviewIndex | null;
publishedWorks: readonly ObservationSessionSummary[];
}): readonly ProjectedLaboratoryValueReview[] {
const reviewed = new Map(index?.items.map((item) => [item.catalogId, item]));
const sessions = new Map(publishedWorks.map((session) => [session.id, session]));
return catalog.map((entry) => {
const review = reviewed.get(entry.id);
if (review?.evidenceId === entry.evidenceId) return reviewedValue(entry, review);
if (entry.id.startsWith("session:")) {
const session = sessions.get(entry.id.slice("session:".length));
if (session) return legacySessionReview(entry, session);
}
return {
catalog: entry,
signal: "unreviewed",
lifecycle: "current",
visualEvidence: "partial",
} satisfies ProjectedLaboratoryValueReview;
});
}
function statusMap(
reviews: readonly ProjectedLaboratoryValueReview[],
): ReadonlyMap<LaboratoryWorkId, ProjectedLaboratorySignal> {
return new Map(reviews.map((review) => [review.catalog.id, review.signal]));
}
export function profileOptionsWithSignals(
options: readonly LaboratoryOption<LaboratoryProfileId>[],
catalog: readonly LaboratoryCatalogEntry[],
reviews: readonly ProjectedLaboratoryValueReview[],
): readonly LaboratoryOption<LaboratoryProfileId>[] {
const signals = statusMap(reviews);
return options.map((option) => {
const latest = catalog.find((entry) => entry.profileId === option.id);
return { ...option, status: latest ? signals.get(latest.id) ?? "unreviewed" : "unreviewed" };
});
}
export function experimentOptionsWithSignals(
options: readonly LaboratoryOption<LaboratoryExperimentId>[],
profileId: LaboratoryProfileId,
catalog: readonly LaboratoryCatalogEntry[],
reviews: readonly ProjectedLaboratoryValueReview[],
): readonly LaboratoryOption<LaboratoryExperimentId>[] {
const signals = statusMap(reviews);
return options.map((option) => {
const latest = catalog.find((entry) => (
entry.profileId === profileId && entry.experimentId === option.id
));
return { ...option, status: latest ? signals.get(latest.id) ?? "unreviewed" : "unreviewed" };
});
}
export function workOptionsWithSignals(
options: readonly LaboratoryOption<LaboratoryWorkId>[],
reviews: readonly ProjectedLaboratoryValueReview[],
): readonly LaboratoryOption<LaboratoryWorkId>[] {
const signals = statusMap(reviews);
return options.map((option) => ({
...option,
status: signals.get(option.id) ?? "unreviewed",
}));
}
@@ -0,0 +1,97 @@
import type {
LaboratoryMethod,
LaboratoryMethodComponent,
} from "../../components/laboratory/LaboratoryPresentation";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
export function digestFromContentId(value: string | null | undefined): string | null {
const digest = value?.split("-").at(-1) ?? "";
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
}
export 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,
},
],
};
}
@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import {
fetchLaboratoryEvidenceReport,
type LaboratoryEvidenceReport,
} from "../../core/laboratory/evidenceReport";
export function useLaboratoryEvidenceReport({
workId,
resultId,
enabled,
}: {
workId: string;
resultId: string;
enabled: boolean;
}): {
report: LaboratoryEvidenceReport | null;
loading: boolean;
error: string | null;
} {
const [report, setReport] = useState<LaboratoryEvidenceReport | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!enabled) return;
const controller = new AbortController();
setReport(null);
setLoading(true);
setError(null);
void fetchLaboratoryEvidenceReport({ workId, resultId, signal: controller.signal })
.then(setReport)
.catch((caught: unknown) => {
if (controller.signal.aborted) return;
setError(caught instanceof Error ? caught.message : "Evidence-report LAB недоступен.");
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, [enabled, resultId, workId]);
return { report, loading, error };
}
@@ -0,0 +1,35 @@
import { useEffect, useState } from "react";
import {
fetchLaboratoryValueReviewIndex,
type LaboratoryValueReviewIndex,
} from "../../core/laboratory/valueReviewIndex";
export function useLaboratoryValueReviewIndex(): {
index: LaboratoryValueReviewIndex | null;
loading: boolean;
error: string | null;
} {
const [index, setIndex] = useState<LaboratoryValueReviewIndex | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
void fetchLaboratoryValueReviewIndex({ signal: controller.signal })
.then(setIndex)
.catch((caught: unknown) => {
if (controller.signal.aborted) return;
setIndex(null);
setError(caught instanceof Error ? caught.message : "Value-review индекс LAB недоступен.");
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
return () => controller.abort();
}, []);
return { index, loading, error };
}
@@ -0,0 +1,24 @@
import { useCallback, useEffect, useState } from "react";
import type { LaboratoryViewAction } from "../contracts";
export type LaboratoryViewMode = "laboratory" | "report";
export function useLaboratoryViewMode(
onActionChange: (action: LaboratoryViewAction | null) => void,
): [LaboratoryViewMode, (next: LaboratoryViewMode) => void] {
const [mode, setMode] = useState<LaboratoryViewMode>("laboratory");
const toggle = useCallback(() => {
setMode((current) => current === "laboratory" ? "report" : "laboratory");
}, []);
useEffect(() => {
onActionChange({
label: mode === "laboratory" ? "Отчёт" : "Лабораторные контуры",
onClick: toggle,
});
return () => onActionChange(null);
}, [mode, onActionChange, toggle]);
return [mode, setMode];
}