feat(lab): separate evidence catalogs and record methods

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 17:56:23 +03:00
parent e9ff29297b
commit 4db00b53f7
17 changed files with 670 additions and 24 deletions
@@ -122,6 +122,7 @@ export function ObservationSessionSelect({
const [deleteTarget, setDeleteTarget] = useState<ObservationSessionSummary | null>(null); const [deleteTarget, setDeleteTarget] = useState<ObservationSessionSummary | null>(null);
const sessions = useObservationSessions({ const sessions = useObservationSessions({
limit, limit,
scope: "source",
replayEnabled: blockedReason === null, replayEnabled: blockedReason === null,
onReplayBegin, onReplayBegin,
onReplayAccepted, onReplayAccepted,
@@ -321,14 +322,13 @@ export function ObservationSessionArchive({
const [deleteTarget, setDeleteTarget] = useState<ObservationSessionSummary | null>(null); const [deleteTarget, setDeleteTarget] = useState<ObservationSessionSummary | null>(null);
const sessions = useObservationSessions({ const sessions = useObservationSessions({
limit, limit,
scope: labsOnly ? "laboratory" : "source",
replayEnabled: blockedReason === null, replayEnabled: blockedReason === null,
onReplayBegin, onReplayBegin,
onReplayAccepted, onReplayAccepted,
onReplaySettled, onReplaySettled,
}); });
const items = labsOnly const items = sessions.items;
? sessions.items.filter((session) => session.lab !== null)
: sessions.items;
return <> return <>
<section <section
@@ -15,6 +15,8 @@ export interface E29EvidenceResult {
frameCount: number; frameCount: number;
timelineStartSeconds: number; timelineStartSeconds: number;
timelineEndSeconds: number; timelineEndSeconds: number;
profileId: string;
producerSha256: string;
}; };
metrics: { metrics: {
frames: { frames: {
@@ -208,6 +210,7 @@ function parseReviewFrame(value: unknown): E29ReviewFrame {
function parseEvidenceResult(value: unknown): E29EvidenceResult { function parseEvidenceResult(value: unknown): E29EvidenceResult {
const source = record(value, "E29 result"); const source = record(value, "E29 result");
const identity = record(source.identity, "E29 identity"); const identity = record(source.identity, "E29 identity");
const profile = record(identity.profile, "E29 identity.profile");
const metrics = record(source.metrics, "E29 metrics"); const metrics = record(source.metrics, "E29 metrics");
const frames = record(metrics.frames, "E29 metrics.frames"); const frames = record(metrics.frames, "E29 metrics.frames");
const semantic = record( const semantic = record(
@@ -253,6 +256,11 @@ function parseEvidenceResult(value: unknown): E29EvidenceResult {
identity.timeline_end_seconds, identity.timeline_end_seconds,
"E29 identity.timeline_end_seconds", "E29 identity.timeline_end_seconds",
), ),
profileId: stringValue(profile.profile_id, "E29 identity.profile.profile_id"),
producerSha256: stringValue(
identity.producer_sha256,
"E29 identity.producer_sha256",
),
}, },
metrics: { metrics: {
frames: { frames: {
@@ -22,6 +22,12 @@ export interface LidarLocalSurfaceModel {
sessionId: string; sessionId: string;
sourcePackId: string; sourcePackId: string;
status: "diagnostic-only"; status: "diagnostic-only";
method: {
executionClass: "deterministic";
pipelineId: string;
algorithm: string;
producerSha256: string;
};
source: { source: {
frameCount: number; frameCount: number;
availableLidarFrames: number; availableLidarFrames: number;
@@ -562,6 +568,8 @@ function temporalQualification(
function model(value: unknown): LidarLocalSurfaceModel { function model(value: unknown): LidarLocalSurfaceModel {
const source = record(value, "LiDAR local-surface model"); const source = record(value, "LiDAR local-surface model");
const sourceEvidence = record(source.source, "source"); const sourceEvidence = record(source.source, "source");
const surfaceModel = record(source.surface_model, "surface_model");
const surfaceProfile = record(surfaceModel.profile, "surface_model.profile");
const metrics = record(source.metrics, "metrics"); const metrics = record(source.metrics, "metrics");
const frames = record(metrics.frames, "metrics.frames"); const frames = record(metrics.frames, "metrics.frames");
if ( if (
@@ -611,6 +619,19 @@ function model(value: unknown): LidarLocalSurfaceModel {
sessionId: text(source.session_id, "session_id", SAFE_ID), sessionId: text(source.session_id, "session_id", SAFE_ID),
sourcePackId: text(source.source_pack_id, "source_pack_id", SAFE_PACK_ID), sourcePackId: text(source.source_pack_id, "source_pack_id", SAFE_PACK_ID),
status: "diagnostic-only", status: "diagnostic-only",
method: {
executionClass: "deterministic",
pipelineId: text(
surfaceProfile.profile_id,
"surface_model.profile.profile_id",
),
algorithm: text(surfaceModel.kind, "surface_model.kind"),
producerSha256: text(
source.producer_sha256,
"producer_sha256",
/^[a-f0-9]{64}$/,
),
},
source: { source: {
frameCount: integer(sourceEvidence.frame_count, "source.frame_count"), frameCount: integer(sourceEvidence.frame_count, "source.frame_count"),
availableLidarFrames: integer( availableLidarFrames: integer(
@@ -9,6 +9,8 @@ export type ObservationSessionStatus =
| "interrupted" | "interrupted"
| "failed"; | "failed";
export type ObservationSessionScope = "all" | "source" | "laboratory";
export interface ObservationLabInstance { export interface ObservationLabInstance {
labId: string; labId: string;
sourceSessionId: string; sourceSessionId: string;
@@ -1077,15 +1079,21 @@ function requirePreparationEtag(
export async function fetchObservationSessionCatalog({ export async function fetchObservationSessionCatalog({
signal, signal,
limit, limit,
scope = "all",
fetcher = globalThis.fetch, fetcher = globalThis.fetch,
}: { }: {
signal?: AbortSignal; signal?: AbortSignal;
limit?: number; limit?: number;
scope?: ObservationSessionScope;
fetcher?: ObservationSessionFetch; fetcher?: ObservationSessionFetch;
} = {}): Promise<ObservationSessionCatalog> { } = {}): Promise<ObservationSessionCatalog> {
const query = Number.isInteger(limit) && Number(limit) >= 1 && Number(limit) <= 100 const queryParameters = new URLSearchParams();
? `?limit=${Number(limit)}` if (Number.isInteger(limit) && Number(limit) >= 1 && Number(limit) <= 100) {
: ""; queryParameters.set("limit", String(Number(limit)));
}
if (scope !== "all") queryParameters.set("scope", scope);
const serializedQuery = queryParameters.toString();
const query = serializedQuery ? `?${serializedQuery}` : "";
let response: Response; let response: Response;
try { try {
response = await fetcher(`/api/v1/observation-sessions${query}`, { response = await fetcher(`/api/v1/observation-sessions${query}`, {
@@ -9,6 +9,7 @@ import {
type ObservationSessionFetch, type ObservationSessionFetch,
type ObservationSessionPreparation, type ObservationSessionPreparation,
type ObservationSessionReplayLaunch, type ObservationSessionReplayLaunch,
type ObservationSessionScope,
type ObservationSessionSummary, type ObservationSessionSummary,
} from "./sessionArchive"; } from "./sessionArchive";
@@ -369,12 +370,14 @@ export function clearObservationReplayPreparation(
export function useObservationSessions({ export function useObservationSessions({
limit = 100, limit = 100,
scope = "all",
replayEnabled = true, replayEnabled = true,
onReplayBegin, onReplayBegin,
onReplayAccepted, onReplayAccepted,
onReplaySettled, onReplaySettled,
}: { }: {
limit?: number; limit?: number;
scope?: ObservationSessionScope;
replayEnabled?: boolean; replayEnabled?: boolean;
/** Called only after the archive is ready, immediately before replacing the old viewer. */ /** Called only after the archive is ready, immediately before replacing the old viewer. */
onReplayBegin?: ( onReplayBegin?: (
@@ -425,7 +428,10 @@ export function useObservationSessions({
const sequence = ++catalogSequence.current; const sequence = ++catalogSequence.current;
if (foreground) setState("loading"); if (foreground) setState("loading");
try { try {
const catalog = await fetchObservationSessionCatalog({ limit: safeLimit }); const catalog = await fetchObservationSessionCatalog({
limit: safeLimit,
scope,
});
if (!mounted.current || sequence !== catalogSequence.current) return false; if (!mounted.current || sequence !== catalogSequence.current) return false;
setItems(catalog.items.slice(0, safeLimit)); setItems(catalog.items.slice(0, safeLimit));
setState("ready"); setState("ready");
@@ -437,7 +443,7 @@ export function useObservationSessions({
setError(errorMessage(loadError)); setError(errorMessage(loadError));
return false; return false;
} }
}, [safeLimit]); }, [safeLimit, scope]);
const refresh = useCallback(() => loadCatalog(true), [loadCatalog]); const refresh = useCallback(() => loadCatalog(true), [loadCatalog]);
@@ -2466,6 +2466,7 @@
} }
.laboratory-task, .laboratory-task,
.laboratory-method,
.laboratory-result-summary { .laboratory-result-summary {
border-radius: 1rem; border-radius: 1rem;
background: rgb(255 255 255 / 0.025); background: rgb(255 255 255 / 0.025);
@@ -2473,6 +2474,7 @@
} }
.laboratory-task > header, .laboratory-task > header,
.laboratory-method > header,
.laboratory-result-summary > header { .laboratory-result-summary > header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -2483,12 +2485,16 @@
.laboratory-task h2, .laboratory-task h2,
.laboratory-task p, .laboratory-task p,
.laboratory-task dl, .laboratory-task dl,
.laboratory-method h2,
.laboratory-method p,
.laboratory-method ul,
.laboratory-result-summary h2, .laboratory-result-summary h2,
.laboratory-result-summary p { .laboratory-result-summary p {
margin: 0; margin: 0;
} }
.laboratory-task h2, .laboratory-task h2,
.laboratory-method h2,
.laboratory-result-summary h2 { .laboratory-result-summary h2 {
margin-top: 0.3rem; margin-top: 0.3rem;
color: var(--nodedc-text-primary); color: var(--nodedc-text-primary);
@@ -2497,6 +2503,7 @@
} }
.laboratory-task p, .laboratory-task p,
.laboratory-method p,
.laboratory-result-summary > p { .laboratory-result-summary > p {
max-width: 66rem; max-width: 66rem;
margin-top: 0.38rem; margin-top: 0.38rem;
@@ -2505,6 +2512,75 @@
line-height: 1.55; line-height: 1.55;
} }
.laboratory-method__summary {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.4rem;
margin-top: 0.9rem;
}
.laboratory-method__summary > div {
display: grid;
gap: 0.25rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.035);
padding: 0.7rem;
}
.laboratory-method__summary span,
.laboratory-method li > span,
.laboratory-method small {
color: var(--nodedc-text-muted);
font-size: 0.54rem;
}
.laboratory-method__summary strong {
color: var(--nodedc-text-primary);
font-size: 0.7rem;
}
.laboratory-method ul {
display: grid;
gap: 0.35rem;
margin-top: 0.55rem;
padding: 0;
list-style: none;
}
.laboratory-method li {
display: grid;
grid-template-columns: 5.5rem minmax(0, 1fr) auto;
align-items: center;
gap: 0.75rem;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.025);
padding: 0.62rem 0.7rem;
}
.laboratory-method li > span {
text-transform: uppercase;
}
.laboratory-method li > div {
display: grid;
min-width: 0;
gap: 0.15rem;
}
.laboratory-method li strong {
overflow: hidden;
color: var(--nodedc-text-primary);
font-size: 0.66rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.laboratory-method code {
color: var(--nodedc-text-secondary);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.54rem;
}
.laboratory-task dl { .laboratory-task dl {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -1204,6 +1204,117 @@ interface LaboratoryOption<T extends string> {
label: string; label: string;
} }
type LaboratoryExecutionClass = "deterministic" | "ai-inference" | "hybrid";
type LaboratoryMethodCompleteness = "complete" | "legacy-partial";
type LaboratoryEvidenceKind = "recorded-replay" | "diagnostic-model";
interface LaboratoryMethodComponent {
kind: "source" | "tool" | "model" | "algorithm" | "runtime";
name: string;
version: string;
role: string;
identitySha256: string | null;
}
interface LaboratoryMethod {
completeness: LaboratoryMethodCompleteness;
executionClass: LaboratoryExecutionClass;
pipelineId: string;
components: readonly LaboratoryMethodComponent[];
}
function digestFromContentId(value: string | null | undefined): string | null {
const digest = value?.split("-").at(-1) ?? "";
return /^[a-f0-9]{64}$/.test(digest) ? digest : null;
}
function publishedLaboratoryMethod(
session: ObservationSessionSummary,
): LaboratoryMethod {
const method = session.lab?.provenance.method;
if (method && typeof method === "object" && !Array.isArray(method)) {
const value = method as Record<string, unknown>;
const rawComponents = Array.isArray(value.components) ? value.components : [];
const components: LaboratoryMethodComponent[] = rawComponents.flatMap((component) => {
if (!component || typeof component !== "object" || Array.isArray(component)) return [];
const item = component as Record<string, unknown>;
const kind = item.kind;
if (
kind !== "source"
&& kind !== "tool"
&& kind !== "model"
&& kind !== "algorithm"
&& kind !== "runtime"
) return [];
if (
typeof item.name !== "string"
|| typeof item.version !== "string"
|| typeof item.role !== "string"
) return [];
return [{
kind: kind as LaboratoryMethodComponent["kind"],
name: item.name,
version: item.version,
role: item.role,
identitySha256: typeof item.identity_sha256 === "string"
? item.identity_sha256
: null,
}];
});
const executionClass = value.execution_class;
const completeness = value.completeness;
if (
components.length
&& typeof value.pipeline_id === "string"
&& (
executionClass === "deterministic"
|| executionClass === "ai-inference"
|| executionClass === "hybrid"
)
&& (completeness === "complete" || completeness === "legacy-partial")
) {
return {
completeness,
executionClass,
pipelineId: value.pipeline_id,
components,
};
}
}
const resultKind = session.lab?.resultKind ?? "unknown";
const algorithmNames: Record<string, string> = {
"e10-integrated-perception": "Camera semantics + LiDAR metric fusion",
"e21-realtime-envelope": "Bounded real-time perception replay",
"e22-temporal-stability": "Temporal 2D/3D/semantic stabilization",
"e23-inline-temporal-stability": "Inline warm-worker stabilization",
"e24-world-motion": "World-frame motion tracking",
"e25-persistent-support-motion": "Persistent occupied-support tracking",
"e26-camera-ego-motion-fusion": "KB4 ego-motion + persistent LiDAR support",
};
return {
completeness: "legacy-partial",
executionClass: "hybrid",
pipelineId: resultKind,
components: [
{
kind: "source",
name: session.lab?.sourceResultId ?? session.lab?.sourceSessionId ?? session.id,
version: "immutable source evidence",
role: "read-only input",
identitySha256: digestFromContentId(session.lab?.sourceResultId),
},
{
kind: "algorithm",
name: algorithmNames[resultKind] ?? resultKind,
version: resultKind,
role: "laboratory derivative",
identitySha256: session.lab?.configSha256 ?? null,
},
],
};
}
function LaboratorySelector<T extends string>({ function LaboratorySelector<T extends string>({
eyebrow, eyebrow,
title, title,
@@ -1285,17 +1396,20 @@ function LaboratoryTask({
function LaboratoryEvidence({ function LaboratoryEvidence({
eyebrow, eyebrow,
title, title,
kind,
resizable = false, resizable = false,
children, children,
}: { }: {
eyebrow: string; eyebrow: string;
title: string; title: string;
kind: LaboratoryEvidenceKind;
resizable?: boolean; resizable?: boolean;
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<section <section
className="lab-result-surface" className="lab-result-surface"
data-evidence-kind={kind}
data-resizable={resizable ? "true" : undefined} data-resizable={resizable ? "true" : undefined}
> >
<header> <header>
@@ -1309,13 +1423,69 @@ function LaboratoryEvidence({
); );
} }
function LaboratoryMethodCard({ method }: { method: LaboratoryMethod }) {
const complete = method.completeness === "complete";
const executionLabels: Record<LaboratoryExecutionClass, string> = {
deterministic: "Детерминированный",
"ai-inference": "AI inference",
hybrid: "Гибридный",
};
return (
<section className="laboratory-method">
<header>
<div>
<span className="section-eyebrow">МЕТОД И ВОСПРОИЗВОДИМОСТЬ</span>
<h2>{method.pipelineId}</h2>
<p>
Зафиксированы вычислительный класс, инструменты, модели и алгоритмы.
{complete
? " Идентичности достаточны для повторного запуска."
: " Это legacy-прогон: отсутствующие исторические версии не восстановлены задним числом."}
</p>
</div>
<StatusBadge tone={complete ? "success" : "warning"}>
{complete ? "Метод полный" : "Legacy · частично"}
</StatusBadge>
</header>
<div className="laboratory-method__summary">
<div>
<span>Класс вычисления</span>
<strong>{executionLabels[method.executionClass]}</strong>
</div>
<div>
<span>Компонентов</span>
<strong>{method.components.length}</strong>
</div>
</div>
<ul>
{method.components.map((component, index) => (
<li key={`${component.kind}:${component.name}:${index}`}>
<span>{component.kind}</span>
<div>
<strong>{component.name}</strong>
<small>{component.role} · {component.version}</small>
</div>
<code>
{component.identitySha256
? component.identitySha256.slice(0, 12)
: "identity не зафиксирована"}
</code>
</li>
))}
</ul>
</section>
);
}
function LaboratoryWorkTemplate({ function LaboratoryWorkTemplate({
task, task,
method,
evidence, evidence,
result = null, result = null,
details = null, details = null,
}: { }: {
task: ReactNode; task: ReactNode;
method: ReactNode;
evidence: ReactNode; evidence: ReactNode;
result?: ReactNode; result?: ReactNode;
details?: ReactNode; details?: ReactNode;
@@ -1323,6 +1493,7 @@ function LaboratoryWorkTemplate({
return ( return (
<div className="laboratory-work-template"> <div className="laboratory-work-template">
{task} {task}
{method}
{evidence} {evidence}
{result} {result}
{details} {details}
@@ -1406,10 +1577,45 @@ function E29LaboratoryResult({
]} ]}
/> />
)} )}
method={(
<LaboratoryMethodCard
method={{
completeness: "complete",
executionClass: "hybrid",
pipelineId: result.identity.profileId,
components: [
{
kind: "source",
name: result.linkedEvidence.sourceResultId,
version: "camera-first semantic observations",
role: "semantic identity and class",
identitySha256: digestFromContentId(result.linkedEvidence.sourceResultId),
},
{
kind: "algorithm",
name: "Camera/LiDAR local-surface validation",
version: result.identity.profileId,
role: "range, occupied support and conflict classification",
identitySha256: result.identity.producerSha256,
},
{
kind: "model",
name: result.linkedEvidence.localSurfaceModelId,
version: "L2.6 local surface",
role: "independent metric geometry",
identitySha256: digestFromContentId(
result.linkedEvidence.localSurfaceModelId,
),
},
],
}}
/>
)}
evidence={( evidence={(
<LaboratoryEvidence <LaboratoryEvidence
eyebrow="ИСХОДНЫЕ ДАННЫЕ" eyebrow="ИСХОДНЫЕ ДАННЫЕ"
title="LiDAR, траектория и камера RAVNOVES00" title="LiDAR, траектория и камера RAVNOVES00"
kind="recorded-replay"
resizable resizable
> >
{replayReady ? ( {replayReady ? (
@@ -1592,10 +1798,14 @@ function PublishedLaboratoryResult({
]} ]}
/> />
)} )}
method={(
<LaboratoryMethodCard method={publishedLaboratoryMethod(session)} />
)}
evidence={( evidence={(
<LaboratoryEvidence <LaboratoryEvidence
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО" eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
title="Исходная запись выбранной лабораторной работы" title="Исходная запись выбранной лабораторной работы"
kind="recorded-replay"
resizable resizable
> >
{replayReady ? ( {replayReady ? (
@@ -1900,10 +2110,43 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
]} ]}
/> />
)} )}
method={(
<LaboratoryMethodCard
method={{
completeness: "complete",
executionClass: e28Model?.method.executionClass ?? "deterministic",
pipelineId: e28Model?.method.pipelineId ?? "local-surface/unavailable",
components: [
{
kind: "source",
name: e28Model?.sourcePackId ?? "Источник не загружен",
version: "immutable vendor MAP + pose",
role: "read-only LiDAR evidence",
identitySha256: digestFromContentId(e28Model?.sourcePackId),
},
{
kind: "algorithm",
name: e28Model?.method.algorithm ?? "Rolling local surface",
version: e28Model?.method.pipelineId ?? "—",
role: "robust local plane, occupancy and temporal residuals",
identitySha256: e28Model?.method.producerSha256 ?? null,
},
{
kind: "runtime",
name: "Mission Core worker D",
version: "recorded-source-paced shadow",
role: "bounded passive replay",
identitySha256: null,
},
],
}}
/>
)}
evidence={( evidence={(
<LaboratoryEvidence <LaboratoryEvidence
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО" eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
title="Диагностическая поверхность и кадры LAB E28" title="Диагностическая поверхность и кадры LAB E28"
kind="diagnostic-model"
> >
<LidarQualityWorkspace <LidarQualityWorkspace
embedded embedded
@@ -21,6 +21,10 @@ function result(overrides = {}) {
frame_count: 4489, frame_count: 4489,
timeline_start_seconds: 35.4, timeline_start_seconds: 35.4,
timeline_end_seconds: 484.0, timeline_end_seconds: 484.0,
profile: {
profile_id: "camera-first-local-surface-validation/v1",
},
producer_sha256: "e".repeat(64),
}, },
metrics: { metrics: {
frames: { frames: {
@@ -57,7 +57,13 @@ function model(overrides = {}) {
timeline_start_seconds: 1, timeline_start_seconds: 1,
timeline_end_seconds: 2, timeline_end_seconds: 2,
}, },
surface_model: {}, surface_model: {
kind: "time-varying-rolling-local-plane",
profile: {
profile_id: "k1-vendor-map-dynamic-local-surface/v1",
},
},
producer_sha256: "e".repeat(64),
occupancy_policy: policy(), occupancy_policy: policy(),
metrics: { metrics: {
frames: { frames: {
@@ -197,6 +197,25 @@ test("data recordings keep the compact session dropdown and laboratory results s
assert.match(workspaceSource, /e29-camera-geometry/); assert.match(workspaceSource, /e29-camera-geometry/);
}); });
test("source and laboratory catalogs are requested as disjoint backend projections", async () => {
const calls = [];
const fetcher = async (input) => {
calls.push(String(input));
return new Response(JSON.stringify({ items: [] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
await fetchObservationSessionCatalog({ limit: 100, scope: "source", fetcher });
await fetchObservationSessionCatalog({ limit: 100, scope: "laboratory", fetcher });
assert.deepEqual(calls, [
"/api/v1/observation-sessions?limit=100&scope=source",
"/api/v1/observation-sessions?limit=100&scope=laboratory",
]);
});
test("session catalog exposes authoritative background preparation state", () => { test("session catalog exposes authoritative background preparation state", () => {
const catalog = decodeObservationSessionCatalog({ const catalog = decodeObservationSessionCatalog({
items: [session({ items: [session({
@@ -45,6 +45,9 @@ test("every laboratory result uses the shared evidence template", async () => {
assert.match(source, /function LaboratoryWorkTemplate\(/); assert.match(source, /function LaboratoryWorkTemplate\(/);
assert.match(source, /function LaboratoryEvidence\(/); assert.match(source, /function LaboratoryEvidence\(/);
assert.match(source, /function LaboratoryMethodCard\(/);
assert.match(source, /method:\s*ReactNode/);
assert.match(source, /data-evidence-kind=\{kind\}/);
assert.match(source, /data-viewer-focused=/); assert.match(source, /data-viewer-focused=/);
assert.match(css, /height:\s*clamp\(42rem,\s*68vh,\s*58rem\)/); assert.match(css, /height:\s*clamp\(42rem,\s*68vh,\s*58rem\)/);
assert.match(css, /resize:\s*vertical/); assert.match(css, /resize:\s*vertical/);
+96
View File
@@ -83,6 +83,53 @@ class PublishedCameraEgoMotionLabInstance:
build: CameraEgoMotionBuild build: CameraEgoMotionBuild
def _laboratory_method(
*,
pipeline_id: str,
execution_class: str,
algorithm: str,
profile_sha256: str | None,
source_result_id: str,
) -> dict[str, object]:
source_identity: str | None = source_result_id.rsplit("-", 1)[-1]
if len(source_identity) != 64 or any(
character not in "0123456789abcdef" for character in source_identity
):
source_identity = None
return {
"schema_version": "missioncore.laboratory-method/v1",
# E19-E26 predate the method manifest. The publisher now records the
# exact known identities, but does not invent historical model/runtime
# versions that were absent from their original accepted evidence.
"completeness": "legacy-partial",
"execution_class": execution_class,
"pipeline_id": pipeline_id,
"components": [
{
"kind": "source",
"name": "immutable accepted upstream result",
"version": "content-addressed",
"role": "read-only input evidence",
"identity_sha256": source_identity,
},
{
"kind": "algorithm",
"name": algorithm,
"version": pipeline_id,
"role": "laboratory derivative",
"identity_sha256": profile_sha256,
},
{
"kind": "tool",
"name": "Mission Core LAB publisher",
"version": "missioncore.lab-instance/v1",
"role": "immutable catalog projection",
"identity_sha256": _sha256(Path(__file__).resolve(strict=True)),
},
],
}
def publish_integrated_lab_instance( def publish_integrated_lab_instance(
*, *,
repository_root: Path, repository_root: Path,
@@ -156,6 +203,13 @@ def publish_integrated_lab_instance(
run_created_at_utc=source.created_at_utc, run_created_at_utc=source.created_at_utc,
provenance={ provenance={
"schema_version": "missioncore.integrated-lab-publication/v1", "schema_version": "missioncore.integrated-lab-publication/v1",
"method": _laboratory_method(
pipeline_id="integrated-perception/v1",
execution_class="hybrid",
algorithm="camera semantics + LiDAR metric fusion",
profile_sha256=profile_sha256,
source_result_id=source.result_id,
),
"storage_mode": "hard-linked-immutable-payloads", "storage_mode": "hard-linked-immutable-payloads",
"source_job_id": source.job.job_id, "source_job_id": source.job.job_id,
"projected_job_id": lab_job.job_id, "projected_job_id": lab_job.job_id,
@@ -263,6 +317,13 @@ def publish_e21_lab_instance(
include_recorded_media=False, include_recorded_media=False,
provenance={ provenance={
"schema_version": "missioncore.e21-lab-publication/v1", "schema_version": "missioncore.e21-lab-publication/v1",
"method": _laboratory_method(
pipeline_id="realtime-envelope/v1",
execution_class="hybrid",
algorithm="bounded real-time perception replay",
profile_sha256=str(e21_report["identity"]["profile_sha256"]),
source_result_id=str(e21_document["result_id"]),
),
"storage_mode": "bounded-derived-replay-and-projection", "storage_mode": "bounded-derived-replay-and-projection",
"e21_result_id": e21_document["result_id"], "e21_result_id": e21_document["result_id"],
"worker_result_id": worker_document["result_id"], "worker_result_id": worker_document["result_id"],
@@ -361,6 +422,13 @@ def publish_e22_lab_instance(
include_recorded_media=False, include_recorded_media=False,
provenance={ provenance={
"schema_version": "missioncore.e22-lab-publication/v1", "schema_version": "missioncore.e22-lab-publication/v1",
"method": _laboratory_method(
pipeline_id="temporal-stability/v1",
execution_class="hybrid",
algorithm="bounded temporal 2D/3D/semantic stabilization",
profile_sha256=build.profile_sha256,
source_result_id=source.result_id,
),
"storage_mode": "bounded-derived-replay-and-temporal-projection", "storage_mode": "bounded-derived-replay-and-temporal-projection",
"source_result_id": source.result_id, "source_result_id": source.result_id,
"source_lab_session_id": (None if source_lab is None else source_lab.session_id), "source_lab_session_id": (None if source_lab is None else source_lab.session_id),
@@ -487,6 +555,13 @@ def publish_e23_lab_instance(
include_recorded_media=False, include_recorded_media=False,
provenance={ provenance={
"schema_version": "missioncore.e23-lab-publication/v1", "schema_version": "missioncore.e23-lab-publication/v1",
"method": _laboratory_method(
pipeline_id="inline-temporal-stability/v1",
execution_class="hybrid",
algorithm="warm-worker inline temporal stabilization",
profile_sha256=profile_sha256,
source_result_id=str(worker_document["result_id"]),
),
"storage_mode": "bounded-inline-worker-result-and-immutable-source-replay", "storage_mode": "bounded-inline-worker-result-and-immutable-source-replay",
"worker_result_id": worker_document["result_id"], "worker_result_id": worker_document["result_id"],
"source_report_sha256": _sha256(source_path), "source_report_sha256": _sha256(source_path),
@@ -589,6 +664,13 @@ def publish_e24_lab_instance(
include_recorded_media=False, include_recorded_media=False,
provenance={ provenance={
"schema_version": "missioncore.e24-lab-publication/v1", "schema_version": "missioncore.e24-lab-publication/v1",
"method": _laboratory_method(
pipeline_id="world-motion/v1",
execution_class="hybrid",
algorithm="world-frame motion tracking",
profile_sha256=build.profile_sha256,
source_result_id=source.result_id,
),
"storage_mode": "bounded-world-frame-tracking-and-immutable-source-replay", "storage_mode": "bounded-world-frame-tracking-and-immutable-source-replay",
"source_result_id": source.result_id, "source_result_id": source.result_id,
"source_lab_session_id": (None if source_lab is None else source_lab.session_id), "source_lab_session_id": (None if source_lab is None else source_lab.session_id),
@@ -695,6 +777,13 @@ def publish_e25_lab_instance(
include_recorded_media=False, include_recorded_media=False,
provenance={ provenance={
"schema_version": "missioncore.e25-lab-publication/v1", "schema_version": "missioncore.e25-lab-publication/v1",
"method": _laboratory_method(
pipeline_id="persistent-support-motion/v1",
execution_class="hybrid",
algorithm="persistent occupied-support tracking",
profile_sha256=build.profile_sha256,
source_result_id=source.result_id,
),
"storage_mode": "bounded-persistent-support-and-immutable-source-replay", "storage_mode": "bounded-persistent-support-and-immutable-source-replay",
"source_result_id": source.result_id, "source_result_id": source.result_id,
"source_lab_session_id": (None if source_lab is None else source_lab.session_id), "source_lab_session_id": (None if source_lab is None else source_lab.session_id),
@@ -826,6 +915,13 @@ def publish_e26_lab_instance(
include_recorded_media=False, include_recorded_media=False,
provenance={ provenance={
"schema_version": "missioncore.e26-lab-publication/v1", "schema_version": "missioncore.e26-lab-publication/v1",
"method": _laboratory_method(
pipeline_id="camera-ego-motion-fusion/v1",
execution_class="hybrid",
algorithm="KB4 multiview ego-motion + persistent LiDAR support",
profile_sha256=build.profile_sha256,
source_result_id=lidar_source.result_id,
),
"storage_mode": ( "storage_mode": (
"bounded-camera-ego-motion-and-immutable-lidar-source-replay" "bounded-camera-ego-motion-and-immutable-lidar-source-replay"
), ),
@@ -1375,6 +1375,7 @@ def k1_local_surface_catalog_item(model: K1LocalSurfaceV1) -> dict[str, object]:
"status": model.report["status"], "status": model.report["status"],
"source": model.report["source"], "source": model.report["source"],
"surface_model": model.report["surface_model"], "surface_model": model.report["surface_model"],
"producer_sha256": model.identity["producer_sha256"],
"occupancy_policy": model.report["occupancy_policy"], "occupancy_policy": model.report["occupancy_policy"],
"metrics": model.report["metrics"], "metrics": model.report["metrics"],
"anchors": model.report["anchors"], "anchors": model.report["anchors"],
+86 -9
View File
@@ -11,7 +11,7 @@ import threading
from collections.abc import Iterator from collections.abc import Iterator
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any, Literal, cast
from uuid import uuid4 from uuid import uuid4
from k1link.artifacts import utc_now_iso from k1link.artifacts import utc_now_iso
@@ -45,6 +45,8 @@ LAB_ARCHIVE_ID = "missioncore.lab-instances"
LAB_ORIGIN = "missioncore.lab-instance/v1" LAB_ORIGIN = "missioncore.lab-instance/v1"
LAB_ID_PATTERN = re.compile(r"^LAB [A-Z][A-Z0-9._-]{0,31}$") LAB_ID_PATTERN = re.compile(r"^LAB [A-Z][A-Z0-9._-]{0,31}$")
SHA256_PATTERN = re.compile(r"^[a-f0-9]{64}$") SHA256_PATTERN = re.compile(r"^[a-f0-9]{64}$")
LAB_METHOD_SCHEMA = "missioncore.laboratory-method/v1"
SessionScope = Literal["all", "source", "laboratory"]
SCHEMA_SQL = """ SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS observation_sessions ( CREATE TABLE IF NOT EXISTS observation_sessions (
@@ -205,27 +207,51 @@ class SessionStore:
connection.commit() connection.commit()
return tuple(imported) return tuple(imported)
def list_recent(self, *, limit: int = 20, cursor: str | None = None) -> SessionPage: def list_recent(
self,
*,
limit: int = 20,
cursor: str | None = None,
scope: SessionScope = "all",
) -> SessionPage:
if not 1 <= limit <= 100: if not 1 <= limit <= 100:
raise ValueError("limit must be within 1..100") raise ValueError("limit must be within 1..100")
scope_clause = {
"all": "1 = 1",
"source": (
"NOT EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
"WHERE lab.session_id = sessions.session_id)"
),
"laboratory": (
"EXISTS (SELECT 1 FROM observation_lab_instances AS lab "
"WHERE lab.session_id = sessions.session_id)"
),
}.get(scope)
if scope_clause is None:
raise ValueError("scope must be all, source, or laboratory")
parameters: list[object] = [] parameters: list[object] = []
where = "" where = f"WHERE {scope_clause}" # noqa: S608 - closed static scope clauses
with self._connect() as connection: with self._connect() as connection:
if cursor is not None: if cursor is not None:
_validate_identifier(cursor, "session cursor") _validate_identifier(cursor, "session cursor")
cursor_row = connection.execute( cursor_row = connection.execute(
"SELECT started_at_utc, session_id FROM observation_sessions " "SELECT sessions.started_at_utc, sessions.session_id "
"WHERE session_id = ?", "FROM observation_sessions AS sessions "
f"WHERE {scope_clause} AND sessions.session_id = ?", # noqa: S608
(cursor,), (cursor,),
).fetchone() ).fetchone()
if cursor_row is None: if cursor_row is None:
raise SessionNotFoundError("observation session cursor was not found") raise SessionNotFoundError("observation session cursor was not found")
where = "WHERE (COALESCE(started_at_utc, ''), session_id) < (COALESCE(?, ''), ?)" where += (
" AND (COALESCE(sessions.started_at_utc, ''), sessions.session_id) "
"< (COALESCE(?, ''), ?)"
)
parameters.extend((cursor_row["started_at_utc"], cursor_row["session_id"])) parameters.extend((cursor_row["started_at_utc"], cursor_row["session_id"]))
parameters.append(limit + 1) parameters.append(limit + 1)
rows = connection.execute( rows = connection.execute(
f"SELECT * FROM observation_sessions {where} " # noqa: S608 - static clause f"SELECT sessions.* FROM observation_sessions AS sessions {where} " # noqa: S608
"ORDER BY COALESCE(started_at_utc, '') DESC, session_id DESC LIMIT ?", "ORDER BY COALESCE(sessions.started_at_utc, '') DESC, "
"sessions.session_id DESC LIMIT ?",
parameters, parameters,
).fetchall() ).fetchall()
lab_rows = ( lab_rows = (
@@ -366,7 +392,9 @@ class SessionStore:
or duration_seconds <= 0 or duration_seconds <= 0
): ):
raise ValueError("LAB duration must be a positive finite value") raise ValueError("LAB duration must be a positive finite value")
serialized_provenance = _serialize_provenance(provenance or {}) normalized_provenance = provenance or {}
_validate_lab_method(normalized_provenance)
serialized_provenance = _serialize_provenance(normalized_provenance)
published_at = utc_now_iso() published_at = utc_now_iso()
with self._lock, self._connect() as connection: with self._lock, self._connect() as connection:
@@ -1078,6 +1106,55 @@ def _serialize_provenance(value: dict[str, Any]) -> str:
return serialized return serialized
def _validate_lab_method(provenance: dict[str, Any]) -> None:
method = provenance.get("method")
if not isinstance(method, dict):
raise ValueError("LAB provenance must include a method manifest")
if method.get("schema_version") != LAB_METHOD_SCHEMA:
raise ValueError("LAB method schema is invalid")
if method.get("completeness") not in {"complete", "legacy-partial"}:
raise ValueError("LAB method completeness is invalid")
if method.get("execution_class") not in {
"deterministic",
"ai-inference",
"hybrid",
}:
raise ValueError("LAB method execution class is invalid")
pipeline_id = method.get("pipeline_id")
if (
not isinstance(pipeline_id, str)
or not pipeline_id.strip()
or len(pipeline_id) > 160
):
raise ValueError("LAB method pipeline id is invalid")
components = method.get("components")
if not isinstance(components, list) or not 1 <= len(components) <= 32:
raise ValueError("LAB method components are invalid")
identities = 0
for component in components:
if not isinstance(component, dict):
raise ValueError("LAB method component is invalid")
if component.get("kind") not in {"source", "tool", "model", "algorithm", "runtime"}:
raise ValueError("LAB method component kind is invalid")
for field in ("name", "version", "role"):
value = component.get(field)
if not isinstance(value, str) or not value.strip() or len(value) > 240:
raise ValueError(f"LAB method component {field} is invalid")
identity = component.get("identity_sha256")
if identity is not None:
if not isinstance(identity, str) or SHA256_PATTERN.fullmatch(identity) is None:
raise ValueError("LAB method component identity is invalid")
identities += 1
if identities == 0:
raise ValueError("LAB method must bind at least one component identity")
if method["completeness"] == "complete" and any(
component.get("identity_sha256") is None
for component in components
if component.get("kind") in {"model", "algorithm"}
):
raise ValueError("complete LAB method must identify every model and algorithm")
def _require_utc_timestamp(value: str, field: str) -> None: def _require_utc_timestamp(value: str, field: str) -> None:
from datetime import datetime from datetime import datetime
+2 -1
View File
@@ -312,10 +312,11 @@ def build_session_router(
def list_observation_sessions( def list_observation_sessions(
limit: int = Query(default=20, ge=1, le=100), limit: int = Query(default=20, ge=1, le=100),
cursor: str | None = Query(default=None, max_length=128), cursor: str | None = Query(default=None, max_length=128),
scope: Literal["all", "source", "laboratory"] = "all",
) -> dict[str, Any]: ) -> dict[str, Any]:
try: try:
_refresh_catalog(catalog_refresher) _refresh_catalog(catalog_refresher)
page = store.list_recent(limit=limit, cursor=cursor) page = store.list_recent(limit=limit, cursor=cursor, scope=scope)
return { return {
"items": [ "items": [
{ {
+29 -2
View File
@@ -43,6 +43,24 @@ from k1link.web.session_api import (
) )
def lab_method() -> dict[str, object]:
return {
"schema_version": "missioncore.laboratory-method/v1",
"completeness": "complete",
"execution_class": "deterministic",
"pipeline_id": "test-pipeline/v1",
"components": [
{
"kind": "algorithm",
"name": "test algorithm",
"version": "v1",
"role": "contract fixture",
"identity_sha256": "9" * 64,
}
],
}
def make_legacy_session(sessions_root: Path, session_id: str) -> Path: def make_legacy_session(sessions_root: Path, session_id: str) -> Path:
session = sessions_root / session_id session = sessions_root / session_id
capture = session / "captures" / "mqtt_live" capture = session / "captures" / "mqtt_live"
@@ -281,17 +299,26 @@ def test_session_router_exposes_immutable_lab_provenance(tmp_path: Path) -> None
source_result_id="e21-realtime-envelope-" + "b" * 64, source_result_id="e21-realtime-envelope-" + "b" * 64,
config_sha256="c" * 64, config_sha256="c" * 64,
run_created_at_utc="2026-07-23T15:55:15.548Z", run_created_at_utc="2026-07-23T15:55:15.548Z",
provenance={"source_payloads_mutated": False}, provenance={
"source_payloads_mutated": False,
"method": lab_method(),
},
) )
router = build_session_router(store) router = build_session_router(store)
list_route = endpoint(router, "/api/v1/observation-sessions", "GET") list_route = endpoint(router, "/api/v1/observation-sessions", "GET")
detail_route = endpoint(router, "/api/v1/observation-sessions/{session_id}", "GET") detail_route = endpoint(router, "/api/v1/observation-sessions/{session_id}", "GET")
listing = list_route(limit=20, cursor=None) listing = list_route(limit=20, cursor=None, scope="all")
item = next(value for value in listing["items"] if value["id"] == binding.session_id) item = next(value for value in listing["items"] if value["id"] == binding.session_id)
source_listing = list_route(limit=20, cursor=None, scope="source")
laboratory_listing = list_route(limit=20, cursor=None, scope="laboratory")
detail = detail_route(session_id=binding.session_id) detail = detail_route(session_id=binding.session_id)
assert item["lab"] == binding.as_dict() assert item["lab"] == binding.as_dict()
assert [value["id"] for value in source_listing["items"]] == [source.name]
assert [value["id"] for value in laboratory_listing["items"]] == [
binding.session_id
]
assert detail["lab"] == binding.as_dict() assert detail["lab"] == binding.as_dict()
assert item["lab"]["source_session_id"] == source.name assert item["lab"]["source_session_id"] == source.name
assert item["lab"]["provenance"]["source_payloads_mutated"] is False assert item["lab"]["provenance"]["source_payloads_mutated"] is False
+53 -3
View File
@@ -23,6 +23,24 @@ from k1link.sessions import (
) )
def lab_method() -> dict[str, object]:
return {
"schema_version": "missioncore.laboratory-method/v1",
"completeness": "complete",
"execution_class": "deterministic",
"pipeline_id": "test-pipeline/v1",
"components": [
{
"kind": "algorithm",
"name": "test algorithm",
"version": "v1",
"role": "contract fixture",
"identity_sha256": "9" * 64,
}
],
}
def make_legacy_session( def make_legacy_session(
sessions_root: Path, sessions_root: Path,
session_id: str, session_id: str,
@@ -798,7 +816,10 @@ def test_lab_instance_is_independent_and_never_deletes_source_evidence(
source_result_id="e21-realtime-envelope-" + "b" * 64, source_result_id="e21-realtime-envelope-" + "b" * 64,
config_sha256="c" * 64, config_sha256="c" * 64,
run_created_at_utc="2026-07-23T15:51:25.000Z", run_created_at_utc="2026-07-23T15:51:25.000Z",
provenance={"storage_mode": "hard-linked-immutable-payloads"}, provenance={
"storage_mode": "hard-linked-immutable-payloads",
"method": lab_method(),
},
) )
detail = store.get_session(binding.session_id) detail = store.get_session(binding.session_id)
@@ -809,6 +830,13 @@ def test_lab_instance_is_independent_and_never_deletes_source_evidence(
assert lab_command.session_id == binding.session_id assert lab_command.session_id == binding.session_id
assert source.is_dir() assert source.is_dir()
assert [item.session_id for item in store.list_recent(scope="source").items] == [
source.name
]
assert [
item.session_id for item in store.list_recent(scope="laboratory").items
] == [binding.session_id]
with pytest.raises(SessionIntegrityError, match="has LAB instances"): with pytest.raises(SessionIntegrityError, match="has LAB instances"):
store.delete_session(source.name) store.delete_session(source.name)
assert source.is_dir() assert source.is_dir()
@@ -838,7 +866,7 @@ def test_lab_instance_publication_is_idempotent_but_provenance_is_immutable(
"source_result_id": "e10-integrated-perception-" + "e" * 64, "source_result_id": "e10-integrated-perception-" + "e" * 64,
"config_sha256": "f" * 64, "config_sha256": "f" * 64,
"run_created_at_utc": "2026-07-23T05:19:43.138Z", "run_created_at_utc": "2026-07-23T05:19:43.138Z",
"provenance": {"source": "accepted"}, "provenance": {"source": "accepted", "method": lab_method()},
} }
first = store.publish_lab_instance(**parameters) first = store.publish_lab_instance(**parameters)
@@ -849,6 +877,28 @@ def test_lab_instance_publication_is_idempotent_but_provenance_is_immutable(
store.publish_lab_instance(**{**parameters, "config_sha256": "0" * 64}) store.publish_lab_instance(**{**parameters, "config_sha256": "0" * 64})
def test_lab_instance_rejects_publication_without_a_method_manifest(
tmp_path: Path,
) -> None:
repository = tmp_path / "repo"
sessions = repository / "sessions"
source = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
store = SessionStore(repository, data_dir=tmp_path / "data")
store.reconcile_archive(xgrids_k1_archive_source(sessions))
with pytest.raises(ValueError, match="method manifest"):
store.publish_lab_instance(
session_id="lab-without-method",
source_session_id=source.name,
display_name="LAB E30 · incomplete method",
lab_id="LAB E30",
result_kind="e30-test",
result_id="e30-test",
run_created_at_utc="2026-07-26T15:00:00Z",
provenance={"schema_version": "legacy"},
)
def test_bounded_lab_instance_excludes_unbounded_recorded_media( def test_bounded_lab_instance_excludes_unbounded_recorded_media(
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
@@ -872,7 +922,7 @@ def test_bounded_lab_instance_excludes_unbounded_recorded_media(
run_created_at_utc="2026-07-23T15:55:15.548Z", run_created_at_utc="2026-07-23T15:55:15.548Z",
duration_seconds=59.962, duration_seconds=59.962,
include_recorded_media=False, include_recorded_media=False,
provenance={"timeline_scope": "bounded"}, provenance={"timeline_scope": "bounded", "method": lab_method()},
) )
detail = store.get_session(binding.session_id) detail = store.get_session(binding.session_id)