import { safeJsonParse, stableStringify } from "@add-ideas/toolbox-helpers"; export const EVIDENCE_SCHEMA = "https://git.add-ideas.de/lotobo/format-lab/schema/evidence-v1" as const; export type EvidenceStatus = "pass" | "change" | "not-tested" | "informational"; export interface EvidenceObservation { id: string; status: EvidenceStatus; expectation?: string; detail: string; } export interface EvidenceReport { schema: typeof EVIDENCE_SCHEMA; contractVersion: 1; generator: { id: string; version: string }; subject: { kind: string; name: string; mediaType?: string }; operation: { id: string; parameters: Record }; limits: Record; observations: EvidenceObservation[]; provenance: { execution: "local-browser"; networkRequired: false; recordedAt: string | null; }; } export function createEvidenceReport( input: Omit & { recordedAt?: string | null; }, ): EvidenceReport { if (!input.generator.id || !input.generator.version) throw new TypeError("Evidence generator identity is required."); if (!input.observations.length || input.observations.length > 1_000) throw new RangeError("Evidence reports need 1–1,000 observations."); const ids = new Set(); for (const observation of input.observations) { if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/u.test(observation.id)) throw new TypeError(`Invalid evidence observation id ${observation.id}.`); if (ids.has(observation.id)) throw new TypeError( `Duplicate evidence observation id ${observation.id}.`, ); ids.add(observation.id); } return { schema: EVIDENCE_SCHEMA, contractVersion: 1, generator: { ...input.generator }, subject: { ...input.subject }, operation: { id: input.operation.id, parameters: structuredClone(input.operation.parameters), }, limits: { ...input.limits }, observations: input.observations.map((item) => ({ ...item })), provenance: { execution: "local-browser", networkRequired: false, recordedAt: input.recordedAt ?? null, }, }; } export function serializeEvidence(report: EvidenceReport): string { return `${stableStringify(report, 2, { maxTextChars: 2 * 1024 * 1024, maxDepth: 24, maxNodes: 20_000, })}\n`; } export function parseEvidence(source: string): EvidenceReport { const value = safeJsonParse(source, { maxTextChars: 2 * 1024 * 1024, maxDepth: 24, maxNodes: 20_000, rejectDangerousKeys: true, }) as unknown; if (!value || Array.isArray(value) || typeof value !== "object") throw new TypeError("Evidence must be a JSON object."); const candidate = value as Partial; if ( candidate.schema !== EVIDENCE_SCHEMA || candidate.contractVersion !== 1 || !candidate.generator || !candidate.subject || !candidate.operation || !candidate.limits || !Array.isArray(candidate.observations) || !candidate.provenance || candidate.provenance.execution !== "local-browser" || candidate.provenance.networkRequired !== false ) throw new TypeError("Unsupported or incomplete format evidence contract."); return createEvidenceReport({ generator: candidate.generator, subject: candidate.subject, operation: candidate.operation, limits: candidate.limits, observations: candidate.observations, recordedAt: candidate.provenance.recordedAt, }); }