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
@@ -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,
};
}