feat(observatory): attest recording equipment and capture profiles

This commit is contained in:
DCCONSTRUCTIONS
2026-09-02 00:57:27 +03:00
parent 7025e173a3
commit d655d6998d
17 changed files with 1089 additions and 42 deletions
@@ -48,10 +48,21 @@ export interface ObservationSessionSummary {
modalities: readonly string[];
durationSeconds: number;
replayable: boolean;
captureAttestation: ObservationCaptureAttestation | null;
preparation: ObservationSessionCatalogPreparation | null;
lab: ObservationLabInstance | null;
}
export interface ObservationCaptureAttestation {
readonly equipmentModelId: string;
readonly equipmentModelSha256: string;
readonly equipmentDisplayName: string;
readonly captureProfileId: string;
readonly captureProfileSha256: string;
readonly captureAttestationSha256: string;
readonly state: "attested" | "attested-with-legacy-evidence" | "unknown";
}
export interface ObservationSessionCatalog {
items: readonly ObservationSessionSummary[];
}
@@ -168,6 +179,7 @@ const ITEM_KEYS = new Set([
"modalities",
"duration_seconds",
"replayable",
"capture_attestation",
"preparation",
"lab",
]);
@@ -187,6 +199,22 @@ const LAB_V3_KEYS = new Set([
...LAB_V2_KEYS,
"calculation_profile",
]);
const CAPTURE_ATTESTATION_KEYS = new Set([
"schema_version",
"equipment_model",
"capture_profile",
"capture_attestation_sha256",
"state",
]);
const EQUIPMENT_MODEL_KEYS = new Set([
"equipment_model_id",
"equipment_model_sha256",
"display_name",
]);
const CAPTURE_PROFILE_KEYS = new Set([
"capture_profile_id",
"capture_profile_sha256",
]);
const CATALOG_PREPARATION_KEYS = new Set([
"preparation_id",
"state",
@@ -400,11 +428,103 @@ function decodeItem(value: unknown, index: number): ObservationSessionSummary {
modalities: requireModalities(value.modalities),
durationSeconds: value.duration_seconds,
replayable: value.replayable,
captureAttestation: decodeCaptureAttestation(value.capture_attestation, id),
preparation: decodeCatalogPreparation(value.preparation, id),
lab: decodeLabInstance(value.lab, id),
};
}
function decodeCaptureAttestation(
value: unknown,
sessionId: string,
): ObservationCaptureAttestation | null {
if (value === null || value === undefined) return null;
if (!isRecord(value)) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} должна быть объектом или null.`,
);
}
assertExactKeys(value, CAPTURE_ATTESTATION_KEYS, `Аттестация записи ${sessionId}`);
if (value.schema_version !== "missioncore.recorded-capture-attestation/v1") {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} имеет неизвестную схему.`,
);
}
if (!isRecord(value.equipment_model) || !isRecord(value.capture_profile)) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} не содержит оборудование или профиль записи.`,
);
}
assertExactKeys(
value.equipment_model,
EQUIPMENT_MODEL_KEYS,
`Оборудование записи ${sessionId}`,
);
assertExactKeys(
value.capture_profile,
CAPTURE_PROFILE_KEYS,
`Профиль записи ${sessionId}`,
);
const equipmentModelId = requireString(
value.equipment_model.equipment_model_id,
"equipment_model_id",
128,
);
const equipmentModelSha256 = requireString(
value.equipment_model.equipment_model_sha256,
"equipment_model_sha256",
64,
);
const captureProfileId = requireString(
value.capture_profile.capture_profile_id,
"capture_profile_id",
128,
);
const captureProfileSha256 = requireString(
value.capture_profile.capture_profile_sha256,
"capture_profile_sha256",
64,
);
const captureAttestationSha256 = requireString(
value.capture_attestation_sha256,
"capture_attestation_sha256",
64,
);
if (
!SAFE_ID.test(equipmentModelId)
|| !SAFE_ID.test(captureProfileId)
|| !SHA256.test(equipmentModelSha256)
|| !SHA256.test(captureProfileSha256)
|| !SHA256.test(captureAttestationSha256)
) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} содержит некорректную immutable identity.`,
);
}
if (
value.state !== "attested"
&& value.state !== "attested-with-legacy-evidence"
&& value.state !== "unknown"
) {
throw new ObservationSessionContractError(
`Аттестация записи ${sessionId} содержит неизвестное состояние.`,
);
}
return {
equipmentModelId,
equipmentModelSha256,
equipmentDisplayName: requireString(
value.equipment_model.display_name,
"equipment display_name",
128,
),
captureProfileId,
captureProfileSha256,
captureAttestationSha256,
state: value.state,
};
}
function decodeLabInstance(
value: unknown,
sessionId: string,
@@ -43,7 +43,7 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
const compatibility = record(row.source_compatibility, "portable source_compatibility");
exactKeys(
compatibility,
["compatible", "outcome", "reason"],
["compatible", "outcome", "reason", "reason_code"],
"portable source_compatibility",
);
const compatible = boolean(compatibility.compatible, "portable compatible");
@@ -53,6 +53,10 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
"portable compatibility outcome",
);
const compatibilityReason = text(compatibility.reason, "portable compatibility reason");
const compatibilityReasonCode = text(
compatibility.reason_code,
"portable compatibility reason_code",
);
const executor = record(row.executor, "portable executor");
exactKeys(
@@ -134,7 +138,7 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
compatible,
reasons: compatible
? []
: [{ code: "source-capability-blocked", message: compatibilityReason }],
: [{ code: compatibilityReasonCode, message: compatibilityReason }],
},
executor: {
contourId: text(executor.contour_id, "portable executor contour_id"),
@@ -198,6 +198,7 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo
modalities: ["point-cloud", "pose"],
durationSeconds: 1_450.744,
replayable: true,
captureAttestation: null,
preparation: null,
lab: null,
}]);
@@ -116,6 +116,7 @@ function portableSetup() {
source_compatibility: {
outcome: "pass",
compatible: true,
reason_code: "source-compatible",
reason: "Запись соответствует требованиям EoMT + DDRNet.",
},
executor: {