feat(perception): publish immutable lab session instances

This commit is contained in:
DCCONSTRUCTIONS
2026-07-23 20:13:40 +03:00
parent f1f8e21b78
commit 10019a454a
16 changed files with 1970 additions and 30 deletions
@@ -9,6 +9,18 @@ export type ObservationSessionStatus =
| "interrupted"
| "failed";
export interface ObservationLabInstance {
labId: string;
sourceSessionId: string;
resultKind: string;
resultId: string;
sourceResultId: string | null;
configSha256: string | null;
runCreatedAtUtc: string;
publishedAtUtc: string;
provenance: Readonly<Record<string, unknown>>;
}
export interface ObservationSessionSummary {
id: string;
label: string;
@@ -19,6 +31,7 @@ export interface ObservationSessionSummary {
durationSeconds: number;
replayable: boolean;
preparation: ObservationSessionCatalogPreparation | null;
lab: ObservationLabInstance | null;
}
export interface ObservationSessionCatalog {
@@ -135,6 +148,18 @@ const ITEM_KEYS = new Set([
"duration_seconds",
"replayable",
"preparation",
"lab",
]);
const LAB_KEYS = new Set([
"lab_id",
"source_session_id",
"result_kind",
"result_id",
"source_result_id",
"config_sha256",
"run_created_at_utc",
"published_at_utc",
"provenance",
]);
const CATALOG_PREPARATION_KEYS = new Set([
"preparation_id",
@@ -344,6 +369,72 @@ function decodeItem(value: unknown, index: number): ObservationSessionSummary {
durationSeconds: value.duration_seconds,
replayable: value.replayable,
preparation: decodeCatalogPreparation(value.preparation, id),
lab: decodeLabInstance(value.lab, id),
};
}
function decodeLabInstance(
value: unknown,
sessionId: string,
): ObservationLabInstance | null {
if (value === null || value === undefined) return null;
if (!isRecord(value)) {
throw new ObservationSessionContractError(
`LAB-привязка сессии ${sessionId} должна быть объектом или null.`,
);
}
assertExactKeys(value, LAB_KEYS, `LAB-привязка сессии ${sessionId}`);
const labId = requireString(value.lab_id, `lab(${sessionId}).lab_id`, 36);
if (!/^LAB [A-Z][A-Z0-9._-]{0,31}$/.test(labId)) {
throw new ObservationSessionContractError("Каталог содержит некорректный LAB-маркер.");
}
const sourceSessionId = requireString(
value.source_session_id,
`lab(${sessionId}).source_session_id`,
128,
);
const resultKind = requireString(value.result_kind, `lab(${sessionId}).result_kind`, 128);
const resultId = requireString(value.result_id, `lab(${sessionId}).result_id`, 128);
const sourceResultId = value.source_result_id === null
? null
: requireString(value.source_result_id, `lab(${sessionId}).source_result_id`, 128);
for (const [field, entry] of [
["source_session_id", sourceSessionId],
["result_kind", resultKind],
["result_id", resultId],
["source_result_id", sourceResultId],
] as const) {
if (entry !== null && !SAFE_ID.test(entry)) {
throw new ObservationSessionContractError(
`Поле lab.${field} содержит небезопасный идентификатор.`,
);
}
}
const configSha256 = value.config_sha256 === null
? null
: requireString(value.config_sha256, `lab(${sessionId}).config_sha256`, 64);
if (configSha256 !== null && !SHA256.test(configSha256)) {
throw new ObservationSessionContractError("LAB-конфигурация не содержит SHA-256.");
}
if (!isRecord(value.provenance)) {
throw new ObservationSessionContractError("LAB provenance должен быть JSON-объектом.");
}
return {
labId,
sourceSessionId,
resultKind,
resultId,
sourceResultId,
configSha256,
runCreatedAtUtc: requireUtcTimestamp(
value.run_created_at_utc,
`lab(${sessionId}).run_created_at_utc`,
),
publishedAtUtc: requireUtcTimestamp(
value.published_at_utc,
`lab(${sessionId}).published_at_utc`,
),
provenance: value.provenance,
};
}