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
+1 -1
View File
@@ -863,6 +863,7 @@ export default function App() {
onDeleteBegin: releaseRecordedReplayForDelete,
}}
onLaboratoryAnnotationActionChange={laboratoryAnnotation.setAction}
onLaboratoryViewActionChange={laboratoryAnnotation.setViewAction}
navigation={{
openView,
openSource,
@@ -1243,7 +1244,6 @@ export default function App() {
]}
/>
</Window>
</>
);
}
@@ -1,12 +1,10 @@
import type { ReactNode } from "react";
import {
Select,
StatusBadge,
} from "@nodedc/ui-react";
import { Select, StatusBadge } from "@nodedc/ui-react";
export interface LaboratoryOption<T extends string> {
id: T;
label: string;
status?: "progress" | "retained" | "failed" | "unreviewed";
}
export type LaboratoryExecutionClass =
@@ -103,6 +101,20 @@ export function LaboratorySelector<T extends string>({
options={options.map((option) => ({
value: option.id,
label: option.label,
icon: option.status ? (
<span
className="laboratory-status-dot"
data-status={option.status}
aria-label={option.status === "progress"
? "Есть подтверждённый прогресс"
: option.status === "retained"
? "Сохранено для сравнения"
: option.status === "failed"
? "Gate не пройден"
: "Требует классификации"}
>
</span>
) : undefined,
}))}
variant="split"
menuWidth="anchor"
@@ -1,26 +1,41 @@
import { useState, type ReactNode } from "react";
import { Button, Icon } from "@nodedc/ui-react";
import type { LaboratoryAnnotationAction } from "../../workspaces/contracts";
import type {
LaboratoryAnnotationAction,
LaboratoryViewAction,
} from "../../workspaces/contracts";
export function useLaboratoryAnnotationHeader(): {
control: ReactNode;
setAction: (action: LaboratoryAnnotationAction | null) => void;
setViewAction: (action: LaboratoryViewAction | null) => void;
} {
const [action, setAction] = useState<LaboratoryAnnotationAction | null>(null);
const [viewAction, setViewAction] = useState<LaboratoryViewAction | null>(null);
return {
setAction,
control: action ? (
<Button
size="compact"
shape="pill"
variant="accent"
icon={<Icon name="edit" size={16} />}
disabled={action.disabled}
onClick={action.onClick}
>
{action.label}
</Button>
setViewAction,
control: action || viewAction ? (
<div className="laboratory-header-tools">
{viewAction ? (
<Button shape="pill" variant="secondary" onClick={viewAction.onClick}>
{viewAction.label}
</Button>
) : null}
{action ? (
<Button
size="compact"
shape="pill"
variant="accent"
icon={<Icon name="edit" size={16} />}
disabled={action.disabled}
onClick={action.onClick}
>
{action.label}
</Button>
) : null}
</div>
) : null,
};
}
@@ -0,0 +1,197 @@
export type LaboratoryEvidenceCompleteness = "recorded" | "not-recorded";
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
export interface LaboratoryEvidenceArtifact {
kind: string | null;
path: string;
byteLength: number;
sha256: string;
schemaVersion: string | null;
mediaType: string | null;
verified: true;
}
export interface LaboratoryEvidenceReport {
schemaVersion: "missioncore.laboratory-evidence-report/v1";
workId: string;
resultId: string;
createdAtUtc: string | null;
access: "read-only";
proof: {
documentSchemaVersion: string;
documentSha256: string;
identitySha256: string;
reportSchemaVersion: string | null;
reportSha256: string | null;
artifactCount: number;
verifiedArtifactCount: number;
};
completeness: Readonly<Record<string, LaboratoryEvidenceCompleteness>>;
identity: Record<string, JsonValue>;
source: Record<string, JsonValue> | null;
configuration: Record<string, JsonValue> | null;
method: Record<string, JsonValue> | null;
execution: Record<string, JsonValue> | null;
resources: Record<string, JsonValue> | null;
metrics: Record<string, JsonValue> | null;
gates: Record<string, JsonValue> | null;
decision: JsonValue | undefined;
limitations: JsonValue | undefined;
authority: Record<string, JsonValue> | null;
artifacts: readonly LaboratoryEvidenceArtifact[];
visualEvidence: Record<string, JsonValue>;
rawReport: Record<string, JsonValue>;
canonicalJson: Record<string, JsonValue>;
}
export class LaboratoryEvidenceReportContractError extends Error {}
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new LaboratoryEvidenceReportContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function nullableRecord(value: unknown, label: string): Record<string, JsonValue> | null {
return value === null ? null : jsonRecord(value, label);
}
function jsonRecord(value: unknown, label: string): Record<string, JsonValue> {
const document = record(value, label);
for (const [key, item] of Object.entries(document)) validateJson(item, `${label}.${key}`);
return document as Record<string, JsonValue>;
}
function validateJson(value: unknown, label: string): asserts value is JsonValue {
if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
if (typeof value === "number" && !Number.isFinite(value)) {
throw new LaboratoryEvidenceReportContractError(`${label}: число не конечно.`);
}
return;
}
if (Array.isArray(value)) {
value.forEach((item, index) => validateJson(item, `${label}[${index}]`));
return;
}
const document = record(value, label);
Object.entries(document).forEach(([key, item]) => validateJson(item, `${label}.${key}`));
}
function text(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new LaboratoryEvidenceReportContractError(`${label}: ожидалась строка.`);
}
return value;
}
function nullableText(value: unknown, label: string): string | null {
return value === null ? null : text(value, label);
}
function integer(value: unknown, label: string): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
throw new LaboratoryEvidenceReportContractError(`${label}: ожидалось целое число.`);
}
return value;
}
function parseArtifact(value: unknown): LaboratoryEvidenceArtifact {
const artifact = record(value, "LAB artifact");
if (artifact.verified !== true) {
throw new LaboratoryEvidenceReportContractError("LAB artifact: хэш не подтверждён.");
}
return {
kind: nullableText(artifact.kind, "LAB artifact kind"),
path: text(artifact.path, "LAB artifact path"),
byteLength: integer(artifact.byte_length, "LAB artifact byte_length"),
sha256: text(artifact.sha256, "LAB artifact sha256"),
schemaVersion: nullableText(artifact.schema_version, "LAB artifact schema_version"),
mediaType: nullableText(artifact.media_type, "LAB artifact media_type"),
verified: true,
};
}
export function parseLaboratoryEvidenceReport(value: unknown): LaboratoryEvidenceReport {
const payload = jsonRecord(value, "LAB evidence report") as Record<string, unknown>;
if (payload.schema_version !== "missioncore.laboratory-evidence-report/v1") {
throw new LaboratoryEvidenceReportContractError("LAB evidence report: неизвестная схема.");
}
if (payload.access !== "read-only") {
throw new LaboratoryEvidenceReportContractError("LAB evidence report: доступ не read-only.");
}
const proof = record(payload.proof, "LAB evidence proof");
const completenessPayload = record(payload.completeness, "LAB evidence completeness");
const completeness: Record<string, LaboratoryEvidenceCompleteness> = {};
for (const [key, state] of Object.entries(completenessPayload)) {
if (state !== "recorded" && state !== "not-recorded") {
throw new LaboratoryEvidenceReportContractError(`LAB completeness ${key}: неизвестное значение.`);
}
completeness[key] = state;
}
if (!Array.isArray(payload.artifacts)) {
throw new LaboratoryEvidenceReportContractError("LAB evidence artifacts: ожидался список.");
}
return {
schemaVersion: "missioncore.laboratory-evidence-report/v1",
workId: text(payload.work_id, "LAB work_id"),
resultId: text(payload.result_id, "LAB result_id"),
createdAtUtc: nullableText(payload.created_at_utc, "LAB created_at_utc"),
access: "read-only",
proof: {
documentSchemaVersion: text(proof.document_schema_version, "document schema"),
documentSha256: text(proof.document_sha256, "document sha256"),
identitySha256: text(proof.identity_sha256, "identity sha256"),
reportSchemaVersion: nullableText(proof.report_schema_version, "report schema"),
reportSha256: nullableText(proof.report_sha256, "report sha256"),
artifactCount: integer(proof.artifact_count, "artifact_count"),
verifiedArtifactCount: integer(proof.verified_artifact_count, "verified_artifact_count"),
},
completeness,
identity: jsonRecord(payload.identity, "LAB identity"),
source: nullableRecord(payload.source, "LAB source"),
configuration: nullableRecord(payload.configuration, "LAB configuration"),
method: nullableRecord(payload.method, "LAB method"),
execution: nullableRecord(payload.execution, "LAB execution"),
resources: nullableRecord(payload.resources, "LAB resources"),
metrics: nullableRecord(payload.metrics, "LAB metrics"),
gates: nullableRecord(payload.gates, "LAB gates"),
decision: payload.decision as JsonValue | undefined,
limitations: payload.limitations as JsonValue | undefined,
authority: nullableRecord(payload.authority, "LAB authority"),
artifacts: payload.artifacts.map(parseArtifact),
visualEvidence: jsonRecord(payload.visual_evidence, "LAB visual evidence"),
rawReport: jsonRecord(payload.raw_report, "LAB raw report"),
canonicalJson: payload as Record<string, JsonValue>,
};
}
export async function fetchLaboratoryEvidenceReport({
workId,
resultId,
fetcher = fetch,
signal,
}: {
workId: string;
resultId: string;
fetcher?: typeof fetch;
signal?: AbortSignal;
}): Promise<LaboratoryEvidenceReport> {
const response = await fetcher(
`/api/v1/laboratory/evidence-reports/${encodeURIComponent(workId)}/${encodeURIComponent(resultId)}`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new LaboratoryEvidenceReportContractError(
response.status === 404
? "Для этой LAB канонический evidence-report пока не опубликован."
: `Evidence-report LAB не прошёл серверную проверку: HTTP ${response.status}.`,
);
}
const report = parseLaboratoryEvidenceReport(await response.json());
if (report.workId !== workId || report.resultId !== resultId) {
throw new LaboratoryEvidenceReportContractError("Evidence-report не совпадает с выбранной LAB identity.");
}
return report;
}
@@ -0,0 +1,129 @@
export type LaboratoryValueSignal = "progress" | "retained" | "failed";
export type LaboratoryValueLifecycle = "current" | "legacy";
export type LaboratoryVisualEvidence = "available" | "partial" | "missing";
export interface LaboratoryValueReviewEntry {
catalogId: string;
evidenceId: string;
signal: LaboratoryValueSignal;
lifecycle: LaboratoryValueLifecycle;
visualEvidence: LaboratoryVisualEvidence;
}
export interface LaboratoryValueReviewIndex {
reviewedAtUtc: string;
items: readonly LaboratoryValueReviewEntry[];
}
export class LaboratoryValueReviewContractError extends Error {}
const ENTRY_KEYS = [
"access",
"catalog_id",
"evidence_id",
"lifecycle",
"signal",
"visual_evidence",
] as const;
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new LaboratoryValueReviewContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function exactKeys(
value: Record<string, unknown>,
expected: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
if (actual.join("\0") !== [...expected].sort().join("\0")) {
throw new LaboratoryValueReviewContractError(`${label}: нарушен состав полей.`);
}
}
function textValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim() || value !== value.trim()) {
throw new LaboratoryValueReviewContractError(`${label}: ожидалась непустая строка.`);
}
return value;
}
function parseEntry(value: unknown): LaboratoryValueReviewEntry {
const item = objectValue(value, "LAB value-review item");
exactKeys(item, ENTRY_KEYS, "LAB value-review item");
if (item.access !== "read-only") {
throw new LaboratoryValueReviewContractError("LAB value-review item: доступ не read-only.");
}
if (!(["progress", "retained", "failed"] as const).includes(
item.signal as LaboratoryValueSignal,
)) {
throw new LaboratoryValueReviewContractError("LAB value-review item: неизвестный signal.");
}
if (!(["current", "legacy"] as const).includes(
item.lifecycle as LaboratoryValueLifecycle,
)) {
throw new LaboratoryValueReviewContractError("LAB value-review item: неизвестный lifecycle.");
}
if (!(["available", "partial", "missing"] as const).includes(
item.visual_evidence as LaboratoryVisualEvidence,
)) {
throw new LaboratoryValueReviewContractError(
"LAB value-review item: неизвестный visual_evidence.",
);
}
return {
catalogId: textValue(item.catalog_id, "LAB value-review catalog_id"),
evidenceId: textValue(item.evidence_id, "LAB value-review evidence_id"),
signal: item.signal as LaboratoryValueSignal,
lifecycle: item.lifecycle as LaboratoryValueLifecycle,
visualEvidence: item.visual_evidence as LaboratoryVisualEvidence,
};
}
export async function fetchLaboratoryValueReviewIndex({
fetcher = fetch,
signal,
}: {
fetcher?: typeof fetch;
signal?: AbortSignal;
} = {}): Promise<LaboratoryValueReviewIndex> {
const response = await fetcher("/api/v1/laboratory/value-review-index", {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new LaboratoryValueReviewContractError(
`Value-review индекс LAB недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "LAB value-review index");
exactKeys(
payload,
["access", "items", "reviewed_at_utc", "schema_version"],
"LAB value-review index",
);
if (
payload.schema_version !== "missioncore.laboratory-value-review-index/v1"
|| payload.access !== "read-only"
|| !Array.isArray(payload.items)
|| payload.items.length > 128
) {
throw new LaboratoryValueReviewContractError(
"LAB value-review index: нарушен контракт.",
);
}
const items = payload.items.map(parseEntry);
if (new Set(items.map((item) => item.catalogId)).size !== items.length) {
throw new LaboratoryValueReviewContractError(
"LAB value-review index: catalog_id продублирован.",
);
}
return {
reviewedAtUtc: textValue(payload.reviewed_at_utc, "LAB value-review reviewed_at_utc"),
items,
};
}
+1
View File
@@ -7,6 +7,7 @@
@import "./styles/l3-pointpillars-visual-audit.css";
@import "./styles/l34-annotation.css";
@import "./styles/laboratory-reporting.css";
@import "./styles/laboratory-evidence-report.css";
@import "./styles/e34-temporal-layer.css";
@import "./styles/e35-degradation-recovery.css";
@import "./styles/e30-human-review.css";
@@ -0,0 +1,298 @@
.laboratory-header-tools {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.45rem;
}
.laboratory-header-tools > .nodedc-button:first-child {
min-width: 6.9rem;
}
.laboratory-status-dot {
display: inline-block;
width: 0.48rem;
height: 0.48rem;
flex: 0 0 0.48rem;
border-radius: 50%;
background: currentcolor;
color: var(--nodedc-text-muted);
box-shadow: 0 0 0 0.08rem rgb(255 255 255 / 0.04);
}
.laboratory-status-dot[data-status="progress"] {
color: rgb(var(--nodedc-success-rgb));
box-shadow: 0 0 0.38rem rgb(var(--nodedc-success-rgb) / 0.35);
}
.laboratory-status-dot[data-status="retained"] {
color: rgb(var(--nodedc-warning-rgb));
box-shadow: 0 0 0.38rem rgb(var(--nodedc-warning-rgb) / 0.28);
}
.laboratory-status-dot[data-status="failed"],
.laboratory-status-dot[data-status="unreviewed"] {
color: var(--nodedc-text-muted);
}
.laboratory-evidence-report {
display: grid;
gap: 0.8rem;
min-width: 0;
padding-bottom: 1rem;
}
.laboratory-evidence-report__header,
.laboratory-evidence-report__integrity,
.laboratory-evidence-report__section {
border-radius: 1rem;
background: rgb(255 255 255 / 0.025);
padding: 1rem;
}
.laboratory-evidence-report__header,
.laboratory-evidence-report__integrity > header,
.laboratory-evidence-report__artifact-list article > header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.laboratory-evidence-report h2,
.laboratory-evidence-report h3,
.laboratory-evidence-report p,
.laboratory-evidence-report dl,
.laboratory-evidence-report dd,
.laboratory-evidence-report ol {
margin: 0;
}
.laboratory-evidence-report h2 {
margin-top: 0.25rem;
color: var(--nodedc-text-primary);
font-size: 1.02rem;
}
.laboratory-evidence-report h3 {
margin-top: 0.22rem;
color: var(--nodedc-text-primary);
font-size: 0.78rem;
}
.laboratory-evidence-report__header p {
margin-top: 0.32rem;
color: var(--nodedc-text-muted);
font-size: 0.6rem;
}
.laboratory-evidence-report__identity {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.42rem;
margin-top: 0.8rem !important;
}
.laboratory-evidence-report__identity > div,
.laboratory-evidence-report__completeness > div {
min-width: 0;
border-radius: 0.72rem;
background: rgb(255 255 255 / 0.028);
padding: 0.68rem;
}
.laboratory-evidence-report dt {
color: var(--nodedc-text-muted);
font-size: 0.52rem;
text-transform: uppercase;
}
.laboratory-evidence-report__identity dd {
overflow-wrap: anywhere;
margin-top: 0.25rem;
color: var(--nodedc-text-secondary);
font-family: var(--nodedc-font-family-mono, monospace);
font-size: 0.56rem;
line-height: 1.45;
}
.laboratory-evidence-report__completeness {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 0.35rem;
margin-top: 0.45rem !important;
}
.laboratory-evidence-report__completeness dd {
margin-top: 0.22rem;
color: var(--nodedc-text-secondary);
font-size: 0.55rem;
}
.laboratory-evidence-report__completeness [data-state="recorded"] dd {
color: rgb(var(--nodedc-success-rgb));
}
.laboratory-evidence-report__completeness [data-state="not-recorded"] dd {
color: rgb(var(--nodedc-warning-rgb));
}
.laboratory-evidence-report__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
gap: 0.55rem;
}
.laboratory-evidence-report__section {
min-width: 0;
}
.laboratory-evidence-report__section > header {
margin-bottom: 0.7rem;
}
.laboratory-evidence-report__tree {
display: grid;
gap: 0.28rem;
}
.laboratory-evidence-report__tree > div {
display: grid;
grid-template-columns: minmax(7.5rem, 0.38fr) minmax(0, 1fr);
gap: 0.55rem;
border-top: 1px solid rgb(255 255 255 / 0.04);
padding-top: 0.34rem;
}
.laboratory-evidence-report__tree > div:first-child {
border-top: 0;
padding-top: 0;
}
.laboratory-evidence-report__tree[data-depth]:not([data-depth="0"]) > div {
grid-template-columns: minmax(6rem, 0.32fr) minmax(0, 1fr);
}
.laboratory-evidence-report__tree dd,
.laboratory-evidence-report__value,
.laboratory-evidence-report__array {
min-width: 0;
overflow-wrap: anywhere;
color: var(--nodedc-text-secondary);
font-size: 0.59rem;
line-height: 1.48;
}
.laboratory-evidence-report__array {
display: grid;
gap: 0.35rem;
padding-left: 1rem;
}
.laboratory-evidence-report__missing {
color: rgb(var(--nodedc-warning-rgb));
font-size: 0.6rem;
line-height: 1.5;
}
.laboratory-evidence-report__artifact-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.42rem;
}
.laboratory-evidence-report__artifact-list article {
min-width: 0;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.028);
padding: 0.72rem;
}
.laboratory-evidence-report__artifact-list strong {
color: var(--nodedc-text-primary);
font-size: 0.63rem;
}
.laboratory-evidence-report__artifact-list p {
overflow-wrap: anywhere;
margin: 0.34rem 0 !important;
color: var(--nodedc-text-secondary);
font-size: 0.58rem;
}
.laboratory-evidence-report__artifact-list dl {
display: grid;
gap: 0.22rem;
}
.laboratory-evidence-report__artifact-list dl > div {
display: grid;
grid-template-columns: 5rem minmax(0, 1fr);
gap: 0.4rem;
}
.laboratory-evidence-report__artifact-list dd {
overflow-wrap: anywhere;
color: var(--nodedc-text-muted);
font-family: var(--nodedc-font-family-mono, monospace);
font-size: 0.53rem;
}
.laboratory-evidence-report__canonical-json pre {
max-height: 34rem;
overflow: auto;
border-radius: 0.72rem;
background: rgb(0 0 0 / 0.24);
padding: 0.8rem;
color: var(--nodedc-text-secondary);
font-family: var(--nodedc-font-family-mono, monospace);
font-size: 0.55rem;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.laboratory-evidence-report__notice {
display: flex;
align-items: flex-start;
gap: 0.6rem;
border-radius: 0.8rem;
background: rgb(var(--nodedc-warning-rgb) / 0.08);
padding: 0.8rem;
}
.laboratory-evidence-report__notice strong {
color: var(--nodedc-text-primary);
font-size: 0.7rem;
}
.laboratory-evidence-report__notice p {
margin-top: 0.25rem;
color: var(--nodedc-text-secondary);
font-size: 0.6rem;
line-height: 1.5;
}
@media (max-width: 1180px) {
.laboratory-evidence-report__completeness {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.laboratory-evidence-report__header,
.laboratory-evidence-report__integrity > header {
display: grid;
}
.laboratory-evidence-report__grid,
.laboratory-evidence-report__artifact-list {
grid-template-columns: 1fr;
}
.laboratory-evidence-report__identity,
.laboratory-evidence-report__completeness {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@@ -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];
}
@@ -0,0 +1,157 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { readFile } from "node:fs/promises";
import { createServer } from "vite";
let server;
let fetchLaboratoryValueReviewIndex;
let fetchLaboratoryEvidenceReport;
before(async () => {
server = await createServer({ server: { middlewareMode: true }, appType: "custom" });
({ fetchLaboratoryValueReviewIndex } = await server.ssrLoadModule(
"/src/core/laboratory/valueReviewIndex.ts",
));
({ fetchLaboratoryEvidenceReport } = await server.ssrLoadModule(
"/src/core/laboratory/evidenceReport.ts",
));
});
after(async () => {
await server?.close();
});
function payload(overrides = {}) {
return {
schema_version: "missioncore.laboratory-value-review-index/v1",
reviewed_at_utc: "2026-08-05T08:30:00Z",
items: [
{
catalog_id: "e46j-raw-fisheye-realtime",
evidence_id: `e46j-raw-fisheye-realtime-${"a".repeat(64)}`,
signal: "progress",
lifecycle: "current",
visual_evidence: "available",
access: "read-only",
},
],
access: "read-only",
...overrides,
};
}
test("LAB value-review index preserves the reviewed evidence identity", async () => {
const index = await fetchLaboratoryValueReviewIndex({
fetcher: async () => new Response(JSON.stringify(payload()), { status: 200 }),
});
assert.equal(index.items[0].catalogId, "e46j-raw-fisheye-realtime");
assert.match(index.items[0].evidenceId, /^e46j-raw-fisheye-realtime-[a-f0-9]{64}$/);
assert.equal(index.items[0].signal, "progress");
});
test("LAB value-review index rejects extra fields instead of trusting presentation data", async () => {
const document = payload();
document.items[0] = { ...document.items[0], renderer: "local" };
await assert.rejects(
fetchLaboratoryValueReviewIndex({
fetcher: async () => new Response(JSON.stringify(document), { status: 200 }),
}),
/состав полей/,
);
});
test("selected LAB evidence report preserves proof, telemetry and canonical JSON", async () => {
const workId = "e46j-raw-fisheye-realtime";
const resultId = `e46j-raw-fisheye-realtime-${"b".repeat(64)}`;
const report = await fetchLaboratoryEvidenceReport({
workId,
resultId,
fetcher: async () => new Response(JSON.stringify({
schema_version: "missioncore.laboratory-evidence-report/v1",
work_id: workId,
result_id: resultId,
created_at_utc: "2026-08-04T19:37:51.841Z",
access: "read-only",
proof: {
document_schema_version: "missioncore.e46j-result/v1",
document_sha256: "c".repeat(64),
identity_sha256: "b".repeat(64),
report_schema_version: "missioncore.e46j-report/v1",
report_sha256: "d".repeat(64),
artifact_count: 1,
verified_artifact_count: 1,
},
completeness: {
identity: "recorded",
source: "recorded",
configuration: "recorded",
method: "recorded",
execution: "recorded",
resources: "recorded",
metrics: "recorded",
gates: "recorded",
decision: "recorded",
limitations: "recorded",
authority: "recorded",
artifacts: "recorded",
visual_evidence: "recorded",
},
identity: { profile_sha256: "e".repeat(64) },
source: { frame_count: 4489, stream_sha256: "f".repeat(64) },
configuration: { detector: { config_sha256: "8".repeat(64) } },
method: { components: [{ kind: "model", identity_sha256: "a".repeat(64) }] },
execution: { worker_host: "worker-006", gpu_name: "RTX 4090" },
resources: { gpu_utilization_percent: { p95: 49 } },
metrics: { core_capacity_fps: 47.84, core_path_p95_ms: 25.35 },
gates: { passed: true },
decision: { provider_promoted: false },
limitations: ["No temporal identity."],
authority: { commands_enabled: false },
artifacts: [{
kind: "visual-overlay-video",
path: "overlay.mp4",
byte_length: 150563706,
sha256: "9".repeat(64),
schema_version: null,
media_type: "video/mp4",
verified: true,
}],
visual_evidence: { review: { completed: true }, artifacts: [] },
raw_report: { schema_version: "missioncore.e46j-report/v1", metrics: { frame_count: 4489 } },
}), { status: 200 }),
});
assert.equal(report.workId, workId);
assert.equal(report.resultId, resultId);
assert.equal(report.resources.gpu_utilization_percent.p95, 49);
assert.equal(report.artifacts[0].verified, true);
assert.equal(report.canonicalJson.raw_report.metrics.frame_count, 4489);
});
test("LAB report UI uses the panel header contract and canonical controls", async () => {
const root = new URL("../src/", import.meta.url);
const [workspace, header, report, presentation, styles] = await Promise.all([
readFile(new URL("workspaces/laboratory/LaboratoryArchiveWorkspace.tsx", root), "utf8"),
readFile(new URL("components/laboratory/useLaboratoryAnnotationHeader.tsx", root), "utf8"),
readFile(new URL("workspaces/laboratory/LaboratoryEvidenceReportView.tsx", root), "utf8"),
readFile(new URL("components/laboratory/LaboratoryPresentation.tsx", root), "utf8"),
readFile(new URL("styles/laboratory-evidence-report.css", root), "utf8"),
]);
assert.match(workspace, /useLaboratoryViewMode/);
assert.match(workspace, /<LaboratoryEvidenceReportView/);
assert.match(workspace, /selectedCatalog/);
assert.match(header, /Отчёт|viewAction\.label/);
assert.match(header, /<Button/);
assert.doesNotMatch(header, /size="compact"[^>]*viewAction/);
assert.match(report, /CANONICAL JSON/);
assert.match(report, /verifiedArtifactCount/);
assert.doesNotMatch(report, /Открыть LAB/);
assert.match(presentation, /className="laboratory-status-dot"/);
assert.doesNotMatch(presentation, /<Icon name="circle"/);
assert.match(styles, /background: currentcolor/);
assert.doesNotMatch(styles, /#[a-f0-9]{3,8}/i);
});
+244
View File
@@ -0,0 +1,244 @@
{
"schema_version": "missioncore.laboratory-value-review-registry/v1",
"reviewed_at_utc": "2026-08-05T08:30:00Z",
"entries": [
{
"catalog_id": "e28-local-surface",
"evidence_id": "k1-local-surface-23762244c8bdb97de26fb721ac957d7a00bc9a63571ac4cfa4be19c4effc7d55",
"signal": "retained",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e29-camera-geometry",
"evidence_id": "e29-camera-geometry-421a9d930638bef12cd5eb10979a477917fa4a389e655ed95f73ba4bd62e13dc",
"signal": "retained",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e30-evidence-review",
"evidence_id": "e30-materialization-841af926d8d28ab93538c46d8f31278a2234c4d1c12c7dc4dc296b249d59735a",
"signal": "retained",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e31-source-binding",
"evidence_id": "e31-source-qualification-b2460a5eb143688c7eea6821b2277e13aea79868abe81d83f7e78548c119159a",
"signal": "progress",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e32-track-geometry",
"evidence_id": "e32-track-geometry-a14ca0e7fb3850ca0dfa3c41634e1b490a2d58ab74d101afc6d6921fbdb0e6fd",
"signal": "progress",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e33-worker-shadow",
"evidence_id": "e33-worker-shadow-05cc0bb264410fd49536df90e94067ac39731aff0322a8873700d40008a8bb3a",
"signal": "progress",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e34-temporal-layer",
"evidence_id": "e34-temporal-occupied-8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73",
"signal": "progress",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e35-degradation-recovery",
"evidence_id": "e35-degradation-recovery-82bdbd5c5bfde6d932737f077153c3a8472c993c343fcbe8a207c39bfa2a6288",
"signal": "progress",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e37-ravnoves-acceptance",
"evidence_id": "e37-ravnoves-acceptance-01b1efd586f747341c712d82f0907b39436a6f91ae92b1dfae987eca05fd8344",
"signal": "progress",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e38-perception-baseline",
"evidence_id": "e38-perception-baseline-a272f82988cd9a7e071fad94c3e9fb49daf804fdcca523f853445fd3113a62b1",
"signal": "failed",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e39-perception-refinement",
"evidence_id": "e39-perception-refinement-2fd253940c9d9a2fd3b3237f3f0932f81a9f69741935d793771243d5779af464",
"signal": "failed",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e40-perception-product-gate",
"evidence_id": "e40-perception-product-gate-e96eec9fd68c3ffaaee898d46285dd329191267200011680f084c75095b92e9a",
"signal": "failed",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "l3-pointpillars-visual-audit",
"evidence_id": "l3-pointpillars-visual-audit-35a5bfef788e40080cac1b8ca82ba65176f158326fa2fc4570b522c97d6a2f8d",
"signal": "retained",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "l31-pointpillars-ravnoves",
"evidence_id": "l31-pointpillars-ravnoves-80a9715f64ea397222fbcfd700803f9152009e3751caf87e2e9dc5ec6fc01b72",
"signal": "retained",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "l32-pointpillars-camera-review",
"evidence_id": "l32-pointpillars-camera-review-40eca128ea9525e8e8c22bd3e981e40d5b66d2869c2e92636ddbf707ae44127a",
"signal": "failed",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "l33-camera-first-detector-review",
"evidence_id": "l33-camera-first-detector-review-2458f6214a731c06bfb44c6e44d4a32ee10389359debdc412c321220a9cdc010",
"signal": "progress",
"lifecycle": "legacy",
"visual_evidence": "available"
},
{
"catalog_id": "e46-detector-truth-island",
"evidence_id": "e46-detector-truth-island-d8ab2745679636dce374b720b562fce05d6d0a26be3eac88650224d7aa92267d",
"signal": "retained",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46a-ai-engineering-preannotation",
"evidence_id": "e46a-ai-engineering-preannotation-37cab05e1168cd6004202b890f2c7a877ab02d036afd0891a11a4df61d4d26bf",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46b-temporal-motion",
"evidence_id": "e46b-temporal-motion-756a677b184e91ba98fef1c8480aff423190dba8c384937c38b2ad1c2098962d",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46c-full-replay-world-tracks",
"evidence_id": "e46c-full-replay-world-tracks-98ca4aeb9839082be64c1ce375ea773cff290e08cf085d8d24bb29e11d6fba8d",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46d-temporal-failure-audit",
"evidence_id": "e46d-temporal-failure-audit-593871e10d971fab885205239b849cbba54d6648fae9d0e9eca1510886b31107",
"signal": "failed",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46e-ready-stack",
"evidence_id": "e46e-ready-stack-d51fd744a86b0effa8685c7aa86d14dfd1b12e97bc8d68d0f53f467237b976bf",
"signal": "retained",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46f-dashcam-bakeoff",
"evidence_id": "e46f-dashcam-bakeoff-2b888a784ba06d4565d34a91fef58af1fc9090ed9dc298ad318010ff4da64507",
"signal": "failed",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46g-rectified-detector-bakeoff",
"evidence_id": "e46g-rectified-detector-bakeoff-9c4eb44cbb61199db0967bd9048712a0964e2dbf587651c9e0c6d808967675c6",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46h-full-rectified-front-replay",
"evidence_id": "e46h-full-rectified-front-replay-43f9d97c06387ffa3b7aa656c48c3cb475be5c40de1211853ae8cb3f5ff2e28f",
"signal": "failed",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46i-grounding-dino-full-replay",
"evidence_id": "e46i-grounding-dino-full-replay-b3a6779db3e460625bd8510dc311f0ea8d2975c73e8216534d8f168187119a06",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "e46j-raw-fisheye-realtime",
"evidence_id": "e46j-raw-fisheye-realtime-7119ce4344438eaa0e748db65aa044e9f9f4a0a226e5eea037a7180d0bc7ace7",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "l34-right-yolox-truth-island-freeze",
"evidence_id": "l34-right-yolox-truth-island-freeze-5175a03144978b25130019da6d37bceb8c6ed6aa3d0d3a4d2df4483e1e27ae76",
"signal": "retained",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "l34a-assisted-yolox-error-audit",
"evidence_id": "l34a-assisted-yolox-error-audit-c8870828634a90ad5a5b02e5ef63189b319e61e74264f3a0e7d74a1d28693a5d",
"signal": "retained",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "l34b-nested-box-consolidation-shadow",
"evidence_id": "l34b-nested-box-consolidation-shadow-2de7fe44c919964fc5c63666bf8374916b301dbefc08c6daea442824f759eb5b",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "l34c-tile-seam-stitch-shadow",
"evidence_id": "l34c-tile-seam-stitch-shadow-3253b7661e11d0a688e1c6fa200fa6d0eafd9679a0c61c4b1200009c0629696a",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "l34d-cumulative-postprocessing-candidate",
"evidence_id": "l34d-cumulative-postprocessing-candidate-7b549cf2c949625f0c643889ae4a1b99953cf36ffae80697a561ee722392ee95",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "l34e-self-review-diagnostic",
"evidence_id": "l34e-self-review-diagnostic-71fb2882d84b71f891ac381d4e31570eebb42ffa3b3fe3802ed0b56cd3b8202a",
"signal": "retained",
"lifecycle": "current",
"visual_evidence": "available"
},
{
"catalog_id": "l34f-adjudicated-reference",
"evidence_id": "l34f-adjudicated-reference-fe2964ea46fab02fd750ef30302a2b12e4c822a2407ba1137022a3ae26e15e5f",
"signal": "retained",
"lifecycle": "current",
"visual_evidence": "available"
}
]
}
@@ -10,7 +10,8 @@ GUI for every LAB run. The product surface remains compact, readable,
predictable, and based on the approved NODE.DC Design Guideline.
This document governs Control Station product UI, laboratory summaries,
evidence viewers, and the boundary with complete engineering reports in Ops.
evidence viewers, selected-LAB evidence reports, and the boundary with
implementation history and architecture narratives in Ops.
It complements `docs/15_LABORATORY_RUN_CANON.md`, which governs publication and
provenance. New non-LAB surfaces and their placement are governed by
`docs/19_PRODUCT_SURFACE_EXTENSION_PROTOCOL.md`.
@@ -95,6 +96,48 @@ to these components and must not reproduce their classes or DOM structure.
Changing this anatomy is a template-version decision, not a LAB-specific
layout edit.
### Selected-LAB evidence report mode
The LAB content window has two views of the same selected immutable run: the
individual LAB template and its complete evidence report. A canonical text `Button` in
`ApplicationPanel.headerTools` switches between `Отчёт` and `Лабораторные
контуры`; this is not a new root navigation item and it preserves the selected
LAB. The control uses the default canonical height so it aligns with the panel
actions.
`missioncore.laboratory-evidence-report/v1` is generated from the selected
run's verified document and artifact manifest. It must expose, without dropping
the raw report:
- exact work/result identity, schema versions, document/report SHA-256;
- source and preprocessing contract;
- run configuration, method, modules, models, algorithms, and their identities;
- actual worker/runtime and resource telemetry when recorded;
- measured metrics, thresholds, checks, decision, limitations, and retained authority;
- every verified artifact with role, byte length, SHA-256, and media/schema metadata;
- an explicit `recorded` or `not-recorded` completeness state for every section.
The UI must never fill a missing field from review copy or visual inference.
Artifact hash or identity failure closes the report rather than presenting a
partial success.
The separate value-review index is not rendered as the report. It binds an
operator classification to exact immutable evidence identity and carries two
independent dimensions:
- signal: `progress`, `retained`, or `failed` for the declared LAB question;
- lifecycle: `current` or `legacy` for code and architecture treatment.
Green means the bounded LAB question produced confirmed progress, yellow means
the evidence is retained for comparison, and gray means a gate failed or a new
identity is not yet reviewed. None of these colors grants production,
navigation, or safety authority. A new result identity never inherits the
classification of the previous result automatically.
The same signal is projected into catalog selectors with a small filled status
lamp using the established status colors. Useful legacy evidence remains readable while its experiment
implementation may be removed from the product core.
### Canonical summary content
The summary must let an operator understand the evidence before opening the
@@ -188,13 +231,15 @@ Do not render:
- a separate layout because one LAB has a different algorithm.
If information is necessary only for development or governance, place it in
Ops, a report, an ADR, a runbook, or developer tooling.
Ops, an ADR, a runbook, or developer tooling. Evidence needed to verify the
selected LAB belongs in its product evidence report.
## Ops engineering report
The complete report for a LAB or architecture milestone lives in the Mission
Core Ops project. Keep the issue description concise and place the report in
titled structured blocks. Use this canonical order:
The complete implementation history and architecture narrative for a LAB or
milestone lives in the Mission Core Ops project. It links the canonical product
evidence report instead of retyping its metrics. Keep the issue description
concise and place the narrative in titled structured blocks. Use this order:
1. Objective and architecture stage.
2. Decision question and hypothesis.
@@ -209,8 +254,10 @@ titled structured blocks. Use this canonical order:
11. Next stage and authority that remains forbidden.
12. Acceptance checker with short verifiable items.
The product summary is a projection of this report, never a second independent
narrative.
The product summary and selected-LAB evidence report are projections of
immutable runtime evidence, never a second independent narrative. Ops adds
engineering context, ownership, implementation history, and future work; it is
not a substitute for source/runtime/metric/artifact proof.
## Review gate before A3
@@ -0,0 +1,60 @@
# ADR 0037: Identity-bound laboratory value-review index
Date: 2026-08-05
Status: accepted and implemented; product presentation amended by ADR 0038
## Context
Mission Core had immutable LAB artifacts and increasingly complete visual
result pages, but no reviewed cross-LAB classification. A chronological list
could not distinguish a useful negative result, a reusable architectural gain,
an intermediate comparison, and an unreviewed replacement result. Encoding
those distinctions in renderer branches would make the UI another source of
truth and would let stale conclusions survive a new run.
## Decision
`config/laboratory-value-review.json` is a strict versioned review registry. Each
entry references a catalog id and the exact evidence identity it evaluates,
then records only:
- `progress`, `retained`, or `failed` for the bounded LAB question;
- `current` or `legacy` for architecture lifecycle;
- visual-evidence availability;
It intentionally contains no finding, decision, limitation, metric, or report
copy. Those facts belong to immutable evidence and ADR 0038.
The backend parses the file fail-closed and exposes a read-only value-review index.
The frontend joins it to the live evidence catalog only when `evidence_id`
matches. A replacement result therefore becomes `unreviewed`; it cannot inherit
a green or yellow state from the previous artifact. Historical published
sessions use their immutable provenance and explicit benchmark result as a
bounded legacy fallback.
The registry is a classification input for selector status lamps and lifecycle
review. It is not the engineering report and is not rendered by the `Отчёт`
toggle. ADR 0038 assigns that toggle to the selected immutable LAB evidence.
## Classification semantics
- `progress` means the experiment achieved a declared bounded objective or
established a reusable contract. It does not mean production acceptance.
- `retained` means the evidence remains useful for comparison, diagnosis, or a
later review even though it is not a promoted result.
- `failed` means an explicit gate or candidate objective did not pass. A failed
result may still be valuable regression evidence.
- `legacy` permits removal of experiment-specific implementation while keeping
immutable evidence, report metadata, and a compatible renderer/read model.
## Consequences
- Value-review copy is reviewable configuration rather than JSX or inferred UI text.
- New identities fail visibly into an unreviewed state.
- Valuable legacy experiments no longer justify retaining their orchestration
code in Mission Core.
- The value-review index does not execute experiments and does not solve the future
provider/graph/run configuration contract.
- Full implementation history remains in Ops. Verifiable source, runtime,
telemetry, metrics, gates, artifacts, and hashes belong to the selected LAB
evidence report defined by ADR 0038.
@@ -0,0 +1,48 @@
# ADR 0038: Selected immutable LAB evidence report
Date: 2026-08-05
Status: accepted and implemented
## Context
The first `Отчёт` implementation rendered one cross-LAB list with three pieces
of reviewed prose per run. It did not answer the operator's actual question:
what exact source, configuration, modules, runtime, hardware load, measurements,
gates, artifacts, and limitations prove the currently selected LAB result.
Most of those facts already existed in immutable manifests and report artifacts,
but the UI discarded them.
## Decision
`Отчёт` is the evidence report of the currently selected immutable LAB identity.
It never changes selection and never aggregates other runs.
The backend exposes
`GET /api/v1/laboratory/evidence-reports/{work_id}/{result_id}` using
`missioncore.laboratory-evidence-report/v1`. Before projection it verifies:
- the registered work ID, result ID pattern, document schema, and exact result ID;
- the canonical identity SHA-256 and its binding to the result ID;
- normalized artifact paths with no traversal or symlink;
- byte length and SHA-256 of every declared artifact;
- the selected JSON report and runtime artifact from those verified descriptors.
The response preserves the raw report and normalizes source, configuration,
method, execution, resource telemetry, metrics, gates, decision, limitations,
authority, artifacts, and visual evidence. Each section is marked `recorded` or
`not-recorded`. Absence is not inferred as success, failure, or not-applicable.
The value-review registry from ADR 0037 remains a separate identity-bound input
for filled selector status lamps. It cannot supply missing evidence fields.
## Consequences
- The operator can audit one LAB without reading source code.
- A green review lamp cannot hide missing telemetry or artifacts.
- Older runs expose their actual gaps instead of receiving fabricated modern
fields.
- Artifact tampering closes the report with a verification error.
- New LAB implementations must publish the canonical evidence dimensions if
they want a complete report; UI copy cannot compensate for a weak payload.
- Ops remains the place for implementation history, ownership, and planned work,
while product evidence remains machine-verifiable and identity-bound.
+22
View File
@@ -6,10 +6,32 @@ from k1link.laboratory.evidence_registry import (
LaboratoryEvidenceRegistry,
LaboratoryRegistryError,
)
from k1link.laboratory.evidence_report import (
LABORATORY_EVIDENCE_REPORT_SCHEMA,
LaboratoryEvidenceReportError,
LaboratoryEvidenceReportNotFound,
LaboratoryEvidenceReportService,
)
from k1link.laboratory.value_review_registry import (
LABORATORY_VALUE_REVIEW_INDEX_SCHEMA,
LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA,
LaboratoryValueReviewEntry,
LaboratoryValueReviewRegistry,
LaboratoryValueReviewRegistryError,
)
__all__ = [
"LABORATORY_EVIDENCE_DEFINITION_SCHEMA",
"LABORATORY_EVIDENCE_REPORT_SCHEMA",
"LaboratoryEvidenceDefinition",
"LaboratoryEvidenceRegistry",
"LaboratoryEvidenceReportError",
"LaboratoryEvidenceReportNotFound",
"LaboratoryEvidenceReportService",
"LaboratoryRegistryError",
"LABORATORY_VALUE_REVIEW_INDEX_SCHEMA",
"LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA",
"LaboratoryValueReviewEntry",
"LaboratoryValueReviewRegistry",
"LaboratoryValueReviewRegistryError",
]
+443
View File
@@ -0,0 +1,443 @@
from __future__ import annotations
import hashlib
import json
from collections.abc import Callable
from pathlib import Path, PurePosixPath
from typing import Any, Final
from k1link.laboratory.evidence_registry import (
LaboratoryEvidenceDefinition,
LaboratoryEvidenceRegistry,
)
LABORATORY_EVIDENCE_REPORT_SCHEMA: Final = "missioncore.laboratory-evidence-report/v1"
_DOCUMENT_MAX_BYTES: Final = 1024 * 1024
_ARTIFACT_LIMIT: Final = 256
_HASH_CHUNK_BYTES: Final = 1024 * 1024
RuntimeRootProvider = Callable[[], Path | None]
class LaboratoryEvidenceReportError(ValueError):
"""Raised when immutable LAB evidence cannot be verified or projected."""
class LaboratoryEvidenceReportNotFound(LaboratoryEvidenceReportError):
"""Raised when the requested evidence identity is not available."""
class LaboratoryEvidenceReportService:
def __init__(
self,
registry: LaboratoryEvidenceRegistry,
runtime_root_provider: RuntimeRootProvider,
) -> None:
self._definitions = {
definition.work_id: definition for definition in registry.definitions
}
self._runtime_root_provider = runtime_root_provider
def read(self, work_id: str, result_id: str) -> dict[str, object]:
definition = self._definitions.get(work_id)
if definition is None or definition.result_id_pattern.fullmatch(result_id) is None:
raise LaboratoryEvidenceReportNotFound("LAB evidence identity is unknown")
result_root = self._result_root(definition, result_id)
document_path = _safe_file(result_root, definition.document_name)
document_bytes = _read_bounded(document_path, _DOCUMENT_MAX_BYTES, "LAB document")
document = _json_object(document_bytes, "LAB document")
_validate_document(document, definition, result_id)
identity = _object_or_none(document.get("identity"))
identity_sha256 = document.get("identity_sha256")
if identity is None or not isinstance(identity_sha256, str):
raise LaboratoryEvidenceReportError("LAB identity proof is missing")
actual_identity_sha256 = _canonical_sha256(identity)
if actual_identity_sha256 != identity_sha256 or not result_id.endswith(identity_sha256):
raise LaboratoryEvidenceReportError("LAB identity proof is invalid")
artifacts = _verified_artifacts(result_root, document.get("artifacts"))
report_descriptor = _report_descriptor(artifacts)
report = (
_read_json_artifact(result_root, report_descriptor, "LAB report")
if report_descriptor is not None
else document
)
runtime_descriptor = _runtime_descriptor(artifacts)
runtime = (
_read_json_artifact(result_root, runtime_descriptor, "LAB runtime")
if runtime_descriptor is not None
else None
)
source = _first_object(
report.get("source"),
identity.get("source"),
_nested(identity, "profile", "source"),
) or _source_projection(report, identity)
configuration = _configuration_projection(report, identity)
method = _first_object(
report.get("method"),
identity.get("method"),
identity.get("profile"),
)
execution = _first_object(
report.get("execution"),
runtime,
identity.get("execution"),
identity.get("worker"),
report.get("worker"),
)
metrics = _first_object(report.get("metrics"), _nested(runtime, "metrics"))
resources = _first_object(
_nested(report, "metrics", "resources"),
_nested(runtime, "metrics", "resources"),
_nested(runtime, "metrics", "gpu"),
)
gates = _first_object(
report.get("acceptance"),
report.get("quality_gate"),
report.get("acceptance_requirements"),
_nested(runtime, "acceptance"),
)
decision = _json_value_or_none(report.get("decision"))
limitations = _json_value_or_none(report.get("limitations"))
authority = _first_object(
report.get("authority"),
identity.get("authority"),
document.get("authority"),
)
visual_review = _first_object(
report.get("visual_review"),
report.get("visual_evidence"),
)
visual_artifacts = [
artifact
for artifact in artifacts
if _is_visual_artifact(artifact)
]
completeness_values: dict[str, object | None] = {
"identity": identity,
"source": source,
"configuration": configuration,
"method": method,
"execution": execution,
"resources": resources,
"metrics": metrics,
"gates": gates,
"decision": decision,
"limitations": limitations,
"authority": authority,
"artifacts": artifacts or None,
"visual_evidence": visual_review or (visual_artifacts or None),
}
return {
"schema_version": LABORATORY_EVIDENCE_REPORT_SCHEMA,
"work_id": work_id,
"result_id": result_id,
"created_at_utc": _optional_text(document.get("created_at_utc")),
"access": "read-only",
"proof": {
"document_schema_version": document.get("schema_version"),
"document_sha256": hashlib.sha256(document_bytes).hexdigest(),
"identity_sha256": identity_sha256,
"report_schema_version": report.get("schema_version"),
"report_sha256": (
report_descriptor["sha256"] if report_descriptor is not None else None
),
"artifact_count": len(artifacts),
"verified_artifact_count": len(artifacts),
},
"completeness": {
key: "recorded" if value is not None else "not-recorded"
for key, value in completeness_values.items()
},
"identity": identity,
"source": source,
"configuration": configuration,
"method": method,
"execution": execution,
"resources": resources,
"metrics": metrics,
"gates": gates,
"decision": decision,
"limitations": limitations,
"authority": authority,
"artifacts": artifacts,
"visual_evidence": {
"review": visual_review,
"artifacts": visual_artifacts,
},
"raw_report": report,
}
def _result_root(
self,
definition: LaboratoryEvidenceDefinition,
result_id: str,
) -> Path:
configured = self._runtime_root_provider()
if configured is None:
raise LaboratoryEvidenceReportNotFound("LAB runtime root is unavailable")
runtime_root = configured.expanduser().absolute()
if runtime_root.is_symlink():
raise LaboratoryEvidenceReportError("LAB runtime root must not be a symlink")
try:
runtime_root = runtime_root.resolve(strict=True)
except OSError as exc:
raise LaboratoryEvidenceReportNotFound("LAB runtime root is unavailable") from exc
candidate = definition.result_root(runtime_root) / result_id
if candidate.is_symlink():
raise LaboratoryEvidenceReportError("LAB result must not be a symlink")
try:
result_root = candidate.resolve(strict=True)
except OSError as exc:
raise LaboratoryEvidenceReportNotFound("LAB evidence result is unavailable") from exc
if not result_root.is_dir() or not result_root.is_relative_to(runtime_root):
raise LaboratoryEvidenceReportError("LAB evidence result path is invalid")
return result_root
def _validate_document(
document: dict[str, Any],
definition: LaboratoryEvidenceDefinition,
result_id: str,
) -> None:
if document.get("schema_version") != definition.result_schema_version:
raise LaboratoryEvidenceReportError("LAB document schema is invalid")
if document.get("result_id") != result_id:
raise LaboratoryEvidenceReportError("LAB result identity is invalid")
def _verified_artifacts(result_root: Path, value: object) -> list[dict[str, object]]:
if value is None:
return []
if not isinstance(value, list) or len(value) > _ARTIFACT_LIMIT:
raise LaboratoryEvidenceReportError("LAB artifact manifest is invalid")
verified: list[dict[str, object]] = []
for index, item in enumerate(value):
descriptor = _object_or_none(item)
if descriptor is None:
raise LaboratoryEvidenceReportError(f"LAB artifact {index} is invalid")
path_value = descriptor.get("path")
byte_length = descriptor.get("byte_length")
sha256 = descriptor.get("sha256")
if (
not isinstance(path_value, str)
or not isinstance(byte_length, int)
or isinstance(byte_length, bool)
or byte_length < 0
or not isinstance(sha256, str)
or len(sha256) != 64
):
raise LaboratoryEvidenceReportError(f"LAB artifact {index} proof is invalid")
path = _safe_file(result_root, path_value)
if path.stat().st_size != byte_length or _file_sha256(path) != sha256:
raise LaboratoryEvidenceReportError(f"LAB artifact {index} proof does not match")
verified.append(
{
"kind": _optional_text(descriptor.get("role") or descriptor.get("kind")),
"path": path_value,
"byte_length": byte_length,
"sha256": sha256,
"schema_version": _optional_text(descriptor.get("schema_version")),
"media_type": _optional_text(descriptor.get("media_type")),
"verified": True,
}
)
return verified
def _safe_file(root: Path, relative: str) -> Path:
if not isinstance(relative, str) or "\\" in relative:
raise LaboratoryEvidenceReportError("LAB artifact path is invalid")
posix = PurePosixPath(relative)
if (
posix.is_absolute()
or not posix.parts
or str(posix) != relative
or any(part in {"", ".", ".."} for part in posix.parts)
):
raise LaboratoryEvidenceReportError("LAB artifact path is invalid")
candidate = root.joinpath(*posix.parts)
current = root
for part in posix.parts:
current = current / part
if current.is_symlink():
raise LaboratoryEvidenceReportError("LAB artifact must not use symlinks")
try:
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise LaboratoryEvidenceReportError("LAB artifact is missing") from exc
if not resolved.is_file() or not resolved.is_relative_to(root):
raise LaboratoryEvidenceReportError("LAB artifact path escaped its result")
return resolved
def _read_bounded(path: Path, maximum: int, label: str) -> bytes:
if path.stat().st_size > maximum:
raise LaboratoryEvidenceReportError(f"{label} is too large")
try:
return path.read_bytes()
except OSError as exc:
raise LaboratoryEvidenceReportError(f"{label} is unreadable") from exc
def _json_object(value: bytes, label: str) -> dict[str, Any]:
try:
document = json.loads(value)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise LaboratoryEvidenceReportError(f"{label} is invalid JSON") from exc
if not isinstance(document, dict) or not all(isinstance(key, str) for key in document):
raise LaboratoryEvidenceReportError(f"{label} must be an object")
return document
def _read_json_artifact(
root: Path,
descriptor: dict[str, object],
label: str,
) -> dict[str, Any]:
path_value = descriptor["path"]
if not isinstance(path_value, str):
raise LaboratoryEvidenceReportError(f"{label} path is invalid")
encoded = _read_bounded(
_safe_file(root, path_value),
_DOCUMENT_MAX_BYTES,
label,
)
return _json_object(encoded, label)
def _report_descriptor(
artifacts: list[dict[str, object]],
) -> dict[str, object] | None:
return next(
(
artifact
for artifact in artifacts
if "report" in str(artifact.get("kind") or "").lower()
and str(artifact.get("path") or "").lower().endswith(".json")
),
None,
)
def _runtime_descriptor(
artifacts: list[dict[str, object]],
) -> dict[str, object] | None:
return next(
(
artifact
for artifact in artifacts
if "runtime" in str(artifact.get("kind") or "").lower()
and str(artifact.get("path") or "").lower().endswith(".json")
),
None,
)
def _is_visual_artifact(artifact: dict[str, object]) -> bool:
kind = str(artifact.get("kind") or "").lower()
media_type = str(artifact.get("media_type") or "").lower()
suffix = Path(str(artifact.get("path") or "")).suffix.lower()
return (
any(token in kind for token in ("visual", "video", "overlay", "contact-sheet", "image"))
or media_type.startswith(("image/", "video/"))
or suffix in {".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm"}
)
def _canonical_sha256(value: object) -> str:
encoded = json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
digest.update(chunk)
return digest.hexdigest()
def _object_or_none(value: object) -> dict[str, Any] | None:
if isinstance(value, dict) and all(isinstance(key, str) for key in value):
return value
return None
def _first_object(*values: object) -> dict[str, Any] | None:
for value in values:
document = _object_or_none(value)
if document is not None:
return document
return None
def _nested(value: object, *keys: str) -> object:
current = value
for key in keys:
document = _object_or_none(current)
if document is None:
return None
current = document.get(key)
return current
def _json_value_or_none(value: object) -> object | None:
return value if value is not None else None
def _source_projection(*documents: dict[str, Any]) -> dict[str, Any] | None:
keys = {
"camera_source_id",
"frame_count",
"route_frame_count",
"session_id",
"source_display_name",
"source_id",
"source_result_id",
"source_session_id",
"source_sha256",
"stream_sha256",
"upstream",
}
result: dict[str, Any] = {}
for document in documents:
for key, value in document.items():
if key in keys or key.endswith("_source"):
result.setdefault(key, value)
return result or None
def _configuration_projection(
report: dict[str, Any],
identity: dict[str, Any],
) -> dict[str, Any] | None:
keys = {
"analysis_profile",
"configuration",
"detection",
"detector",
"parameters",
"preprocessing",
"profile",
"valid_fov",
}
result: dict[str, Any] = {}
for document in (report, identity):
for key, value in document.items():
if key in keys:
result.setdefault(key, value)
return result or None
def _optional_text(value: object) -> str | None:
return value if isinstance(value, str) and value.strip() else None
@@ -0,0 +1,156 @@
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Literal, cast
LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA: Final = (
"missioncore.laboratory-value-review-registry/v1"
)
LABORATORY_VALUE_REVIEW_INDEX_SCHEMA: Final = "missioncore.laboratory-value-review-index/v1"
_REGISTRY_MAX_BYTES: Final = 128 * 1024
_CATALOG_ID = re.compile(r"^(?:[a-z][a-z0-9-]{2,95}|session:[A-Za-z0-9._:-]{3,128})$")
_EVIDENCE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,191}$")
_ROOT_KEYS: Final = frozenset({"schema_version", "reviewed_at_utc", "entries"})
_ENTRY_KEYS: Final = frozenset(
{"catalog_id", "evidence_id", "signal", "lifecycle", "visual_evidence"}
)
LaboratoryValueSignal = Literal["progress", "retained", "failed"]
LaboratoryValueLifecycle = Literal["current", "legacy"]
LaboratoryVisualEvidence = Literal["available", "partial", "missing"]
class LaboratoryValueReviewRegistryError(ValueError):
"""Raised when reviewed LAB value metadata is unsafe or ambiguous."""
@dataclass(frozen=True, slots=True)
class LaboratoryValueReviewEntry:
catalog_id: str
evidence_id: str
signal: LaboratoryValueSignal
lifecycle: LaboratoryValueLifecycle
visual_evidence: LaboratoryVisualEvidence
def as_payload(self) -> dict[str, str]:
return {
"catalog_id": self.catalog_id,
"evidence_id": self.evidence_id,
"signal": self.signal,
"lifecycle": self.lifecycle,
"visual_evidence": self.visual_evidence,
"access": "read-only",
}
@dataclass(frozen=True, slots=True)
class LaboratoryValueReviewRegistry:
reviewed_at_utc: str
entries: tuple[LaboratoryValueReviewEntry, ...]
def __post_init__(self) -> None:
_text(self.reviewed_at_utc, "reviewed_at_utc", maximum=64)
if not isinstance(self.entries, tuple) or not all(
isinstance(entry, LaboratoryValueReviewEntry) for entry in self.entries
):
raise LaboratoryValueReviewRegistryError(
"LAB value-review entries must be an immutable tuple"
)
catalog_ids = [entry.catalog_id for entry in self.entries]
if len(catalog_ids) != len(set(catalog_ids)):
raise LaboratoryValueReviewRegistryError("duplicate LAB value-review catalog_id")
@classmethod
def from_file(cls, path: Path) -> LaboratoryValueReviewRegistry:
candidate = path.expanduser().absolute()
if candidate.is_symlink() or not candidate.is_file():
raise LaboratoryValueReviewRegistryError(
"LAB value-review registry must be a regular file"
)
if candidate.stat().st_size > _REGISTRY_MAX_BYTES:
raise LaboratoryValueReviewRegistryError("LAB value-review registry is too large")
try:
payload: object = json.loads(candidate.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as exc:
raise LaboratoryValueReviewRegistryError(
"LAB value-review registry is unreadable"
) from exc
document = _object(payload, "LAB value-review registry")
_exact_keys(document, _ROOT_KEYS, "LAB value-review registry")
if document["schema_version"] != LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA:
raise LaboratoryValueReviewRegistryError(
"LAB value-review registry schema is invalid"
)
entries = document["entries"]
if not isinstance(entries, list) or len(entries) > 128:
raise LaboratoryValueReviewRegistryError("LAB value-review entries are invalid")
return cls(
reviewed_at_utc=_text(document["reviewed_at_utc"], "reviewed_at_utc", maximum=64),
entries=tuple(_entry(item, index) for index, item in enumerate(entries)),
)
def as_payload(self) -> dict[str, object]:
return {
"schema_version": LABORATORY_VALUE_REVIEW_INDEX_SCHEMA,
"reviewed_at_utc": self.reviewed_at_utc,
"items": [entry.as_payload() for entry in self.entries],
"access": "read-only",
}
def _entry(value: object, index: int) -> LaboratoryValueReviewEntry:
document = _object(value, f"LAB value-review entry {index}")
_exact_keys(document, _ENTRY_KEYS, f"LAB value-review entry {index}")
catalog_id = _text(document["catalog_id"], "catalog_id", maximum=160)
evidence_id = _text(document["evidence_id"], "evidence_id", maximum=192)
if _CATALOG_ID.fullmatch(catalog_id) is None:
raise LaboratoryValueReviewRegistryError("LAB value-review catalog_id is invalid")
if _EVIDENCE_ID.fullmatch(evidence_id) is None:
raise LaboratoryValueReviewRegistryError("LAB value-review evidence_id is invalid")
signal = document["signal"]
lifecycle = document["lifecycle"]
visual_evidence = document["visual_evidence"]
if signal not in {"progress", "retained", "failed"}:
raise LaboratoryValueReviewRegistryError("LAB value-review signal is invalid")
if lifecycle not in {"current", "legacy"}:
raise LaboratoryValueReviewRegistryError("LAB value-review lifecycle is invalid")
if visual_evidence not in {"available", "partial", "missing"}:
raise LaboratoryValueReviewRegistryError("LAB value-review visual_evidence is invalid")
return LaboratoryValueReviewEntry(
catalog_id=catalog_id,
evidence_id=evidence_id,
signal=cast(LaboratoryValueSignal, signal),
lifecycle=cast(LaboratoryValueLifecycle, lifecycle),
visual_evidence=cast(LaboratoryVisualEvidence, visual_evidence),
)
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise LaboratoryValueReviewRegistryError(f"{label} must be an object")
return value
def _exact_keys(document: dict[str, object], expected: frozenset[str], label: str) -> None:
actual = frozenset(document)
if actual != expected:
raise LaboratoryValueReviewRegistryError(
f"{label} keys are invalid; missing={sorted(expected - actual)}, "
f"unexpected={sorted(actual - expected)}"
)
def _text(value: object, label: str, *, maximum: int = 768) -> str:
if (
not isinstance(value, str)
or not value.strip()
or value != value.strip()
or len(value) > maximum
):
raise LaboratoryValueReviewRegistryError(
f"{label} must be a bounded trimmed string"
)
return value
+10
View File
@@ -57,6 +57,7 @@ from k1link.compute.e40_perception_product_gate import (
)
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
from k1link.web.l31_pointpillars_ravnoves_api import latest_l31_identity
from k1link.web.l32_pointpillars_camera_review_api import latest_l32_identity
from k1link.web.l33_camera_first_detector_review_api import latest_l33_identity
@@ -917,6 +918,15 @@ def build_advanced_laboratory_router(
"access": "read-only",
}
)
l31_identity = latest_l31_identity(l31_ravnoves_root_provider)
if l31_identity is not None:
items.append(
{
"work_id": "l31-pointpillars-ravnoves",
**l31_identity,
"access": "read-only",
}
)
l32_identity = latest_l32_identity(l32_camera_review_root_provider)
if l32_identity is not None:
items.append(
+19 -1
View File
@@ -23,7 +23,11 @@ from k1link.compute import (
RecordedPerceptionOverlayMux,
RecordedPerceptionOverlayStore,
)
from k1link.laboratory import LaboratoryEvidenceRegistry
from k1link.laboratory import (
LaboratoryEvidenceRegistry,
LaboratoryEvidenceReportService,
LaboratoryValueReviewRegistry,
)
from k1link.sessions import (
MaterializedRecording,
RecordedMediaInspector,
@@ -104,6 +108,7 @@ from k1link.web.l34e_self_review_diagnostic_api import (
)
from k1link.web.l34f_adjudication_api import build_l34f_adjudication_router
from k1link.web.laboratory_api import build_laboratory_router
from k1link.web.laboratory_report_api import build_laboratory_report_router
from k1link.web.lidar_api import build_lidar_router
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
from k1link.web.map_api import (
@@ -137,6 +142,13 @@ INVALID_REQUEST_DETAIL = "Некорректные параметры запро
LABORATORY_EVIDENCE_REGISTRY = LaboratoryEvidenceRegistry.from_directory(
REPOSITORY_ROOT / "config" / "laboratories"
)
LABORATORY_VALUE_REVIEW_REGISTRY = LaboratoryValueReviewRegistry.from_file(
REPOSITORY_ROOT / "config" / "laboratory-value-review.json"
)
LABORATORY_EVIDENCE_REPORTS = LaboratoryEvidenceReportService(
LABORATORY_EVIDENCE_REGISTRY,
lambda: REPOSITORY_ROOT / ".runtime" / "compute-experiments",
)
def _resolve_media_tool(name: str) -> Path | None:
@@ -559,6 +571,12 @@ app.include_router(
),
)
)
app.include_router(
build_laboratory_report_router(
LABORATORY_VALUE_REVIEW_REGISTRY,
LABORATORY_EVIDENCE_REPORTS,
)
)
app.include_router(
build_advanced_laboratory_router(
evidence_registry=LABORATORY_EVIDENCE_REGISTRY,
+34
View File
@@ -0,0 +1,34 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException
from k1link.laboratory import (
LaboratoryEvidenceReportError,
LaboratoryEvidenceReportNotFound,
LaboratoryEvidenceReportService,
LaboratoryValueReviewRegistry,
)
def build_laboratory_report_router(
registry: LaboratoryValueReviewRegistry,
evidence_reports: LaboratoryEvidenceReportService | None = None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
@router.get("/value-review-index")
def get_value_review_index() -> dict[str, object]:
return registry.as_payload()
@router.get("/evidence-reports/{work_id}/{result_id}")
def get_evidence_report(work_id: str, result_id: str) -> dict[str, object]:
if evidence_reports is None:
raise HTTPException(status_code=404, detail="LAB evidence report is unavailable")
try:
return evidence_reports.read(work_id, result_id)
except LaboratoryEvidenceReportNotFound as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except LaboratoryEvidenceReportError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
return router
+24
View File
@@ -74,6 +74,30 @@ def test_advanced_index_is_empty_when_not_configured() -> None:
}
def test_advanced_index_includes_valid_l31_identity(
tmp_path: Path,
monkeypatch: MonkeyPatch,
) -> None:
identity = {
"result_id": f"l31-pointpillars-ravnoves-{'a' * 64}",
"created_at_utc": "2026-07-31T10:56:45.861Z",
}
monkeypatch.setattr(advanced_api, "latest_l31_identity", lambda _provider: identity)
router = build_advanced_laboratory_router(
l31_ravnoves_root_provider=lambda: tmp_path,
)
index = _endpoint(router, "/api/v1/laboratory/advanced-index")()
assert index["items"] == [ # type: ignore[index]
{
"work_id": "l31-pointpillars-ravnoves",
**identity,
"access": "read-only",
}
]
def test_registry_index_does_not_follow_a_runtime_symlink(tmp_path: Path) -> None:
actual = tmp_path / "actual"
actual.mkdir()
+159
View File
@@ -0,0 +1,159 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import pytest
from k1link.laboratory import (
LaboratoryEvidenceRegistry,
LaboratoryEvidenceReportError,
LaboratoryEvidenceReportNotFound,
LaboratoryEvidenceReportService,
)
def _canonical_sha256(value: object) -> str:
return hashlib.sha256(
json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
).hexdigest()
def _write_json(path: Path, value: object) -> bytes:
encoded = json.dumps(value, ensure_ascii=False, indent=2).encode("utf-8")
path.write_bytes(encoded)
return encoded
def _service(tmp_path: Path) -> tuple[LaboratoryEvidenceReportService, str, Path]:
definitions = tmp_path / "definitions"
definitions.mkdir()
_write_json(
definitions / "e99-proof.json",
{
"schema_version": "missioncore.laboratory-evidence-definition/v1",
"work_id": "e99-proof",
"evidence": {
"runtime_relative_root": "e99/results",
"result_id_prefix": "e99-proof",
"document_name": "manifest.json",
"schema_version": "missioncore.e99-result/v1",
},
},
)
runtime = tmp_path / "runtime"
results = runtime / "e99" / "results"
results.mkdir(parents=True)
source = {"session_id": "immutable-source", "stream_sha256": "a" * 64}
model = {"name": "detector", "sha256": "b" * 64}
identity = {
"schema_version": "missioncore.e99-result/v1",
"profile": {
"source": source,
"model": model,
},
"worker": {"node": "worker-006", "container": "ndc-worker@sha256:proof"},
"authority": {"commands_enabled": False, "navigation_or_safety_accepted": False},
}
identity_sha256 = _canonical_sha256(identity)
result_id = f"e99-proof-{identity_sha256}"
result_root = results / result_id
result_root.mkdir()
report = {
"schema_version": "missioncore.e99-report/v1",
"source": source,
"method": {"components": [model]},
"metrics": {
"frames": 100,
"resources": {"gpu_utilization_p95_percent": 48.0, "rss_p95_mib": 91.0},
},
"acceptance": {"passed": True, "checks": {"frame_coverage": True}},
"decision": {"promoted": False, "next_action": "temporal gate"},
"limitations": ["No independent ground truth."],
"authority": identity["authority"],
}
report_bytes = _write_json(result_root / "report.json", report)
visual_bytes = b"visual-proof"
(result_root / "visual.png").write_bytes(visual_bytes)
manifest = {
"schema_version": "missioncore.e99-result/v1",
"result_id": result_id,
"identity_sha256": identity_sha256,
"identity": identity,
"created_at_utc": "2026-08-05T09:00:00Z",
"artifacts": [
{
"role": "engineering-report",
"path": "report.json",
"byte_length": len(report_bytes),
"sha256": hashlib.sha256(report_bytes).hexdigest(),
},
{
"role": "visual-contact-sheet",
"path": "visual.png",
"byte_length": len(visual_bytes),
"sha256": hashlib.sha256(visual_bytes).hexdigest(),
},
],
}
_write_json(result_root / "manifest.json", manifest)
registry = LaboratoryEvidenceRegistry.from_directory(definitions)
return LaboratoryEvidenceReportService(registry, lambda: runtime), result_id, result_root
def test_evidence_report_projects_verified_canonical_evidence(tmp_path: Path) -> None:
service, result_id, _ = _service(tmp_path)
report = service.read("e99-proof", result_id)
assert report["schema_version"] == "missioncore.laboratory-evidence-report/v1"
assert report["result_id"] == result_id
assert report["source"]["session_id"] == "immutable-source" # type: ignore[index]
assert report["configuration"]["profile"]["model"]["name"] == "detector" # type: ignore[index]
assert report["resources"]["gpu_utilization_p95_percent"] == 48.0 # type: ignore[index]
assert report["proof"]["verified_artifact_count"] == 2 # type: ignore[index]
assert report["completeness"]["visual_evidence"] == "recorded" # type: ignore[index]
assert report["completeness"]["execution"] == "recorded" # type: ignore[index]
def test_evidence_report_rejects_tampered_artifact(tmp_path: Path) -> None:
service, result_id, result_root = _service(tmp_path)
(result_root / "visual.png").write_bytes(b"tampered")
with pytest.raises(LaboratoryEvidenceReportError, match="does not match"):
service.read("e99-proof", result_id)
def test_evidence_report_rejects_unknown_identity(tmp_path: Path) -> None:
service, _, _ = _service(tmp_path)
with pytest.raises(LaboratoryEvidenceReportNotFound, match="unavailable"):
service.read("e99-proof", f"e99-proof-{'f' * 64}")
def test_evidence_report_marks_missing_fields_without_inventing_them(tmp_path: Path) -> None:
service, result_id, result_root = _service(tmp_path)
report_path = result_root / "report.json"
report = json.loads(report_path.read_text(encoding="utf-8"))
report.pop("metrics")
report.pop("limitations")
report_bytes = _write_json(report_path, report)
manifest_path = result_root / "manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["artifacts"][0]["byte_length"] = len(report_bytes)
manifest["artifacts"][0]["sha256"] = hashlib.sha256(report_bytes).hexdigest()
_write_json(manifest_path, manifest)
evidence = service.read("e99-proof", result_id)
assert evidence["metrics"] is None
assert evidence["limitations"] is None
assert evidence["completeness"]["metrics"] == "not-recorded" # type: ignore[index]
assert evidence["completeness"]["limitations"] == "not-recorded" # type: ignore[index]
@@ -0,0 +1,90 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from fastapi.routing import APIRoute
from k1link.laboratory import (
LaboratoryValueReviewRegistry,
LaboratoryValueReviewRegistryError,
)
from k1link.web.laboratory_report_api import build_laboratory_report_router
def _document() -> dict[str, object]:
return {
"schema_version": "missioncore.laboratory-value-review-registry/v1",
"reviewed_at_utc": "2026-08-05T08:30:00Z",
"entries": [
{
"catalog_id": "e46j-raw-fisheye-realtime",
"evidence_id": f"e46j-raw-fisheye-realtime-{'a' * 64}",
"signal": "progress",
"lifecycle": "current",
"visual_evidence": "available",
}
],
}
def _write(path: Path, document: dict[str, object]) -> None:
path.write_text(json.dumps(document), encoding="utf-8")
def test_value_review_registry_is_strict_and_projects_read_only_index(tmp_path: Path) -> None:
path = tmp_path / "laboratory-value-review.json"
_write(path, _document())
registry = LaboratoryValueReviewRegistry.from_file(path)
router = build_laboratory_report_router(registry)
route = next(
route
for route in router.routes
if isinstance(route, APIRoute)
and route.path == "/api/v1/laboratory/value-review-index"
)
payload = route.endpoint()
assert payload["schema_version"] == "missioncore.laboratory-value-review-index/v1"
assert payload["items"][0]["signal"] == "progress"
assert payload["items"][0]["access"] == "read-only"
def test_value_review_registry_rejects_unknown_keys(tmp_path: Path) -> None:
path = tmp_path / "laboratory-value-review.json"
document = _document()
document["renderer"] = "local"
_write(path, document)
with pytest.raises(LaboratoryValueReviewRegistryError, match="unexpected"):
LaboratoryValueReviewRegistry.from_file(path)
def test_value_review_registry_rejects_duplicate_catalog_ids(tmp_path: Path) -> None:
path = tmp_path / "laboratory-value-review.json"
document = _document()
entries = document["entries"]
assert isinstance(entries, list)
entries.append({**entries[0], "evidence_id": f"duplicate-{'b' * 64}"})
_write(path, document)
with pytest.raises(LaboratoryValueReviewRegistryError, match="duplicate"):
LaboratoryValueReviewRegistry.from_file(path)
def test_product_value_review_registry_covers_reviewed_laboratory_families() -> None:
root = Path(__file__).resolve().parents[1]
registry = LaboratoryValueReviewRegistry.from_file(
root / "config" / "laboratory-value-review.json"
)
assert len(registry.entries) == 34
assert {entry.catalog_id for entry in registry.entries} >= {
"e28-local-surface",
"e46d-temporal-failure-audit",
"e46j-raw-fisheye-realtime",
"l31-pointpillars-ravnoves",
"l34f-adjudicated-reference",
}