Files
format-lab/src/lab/evidence.ts
T
zemion c40a2c4b2a
Verify / verify (push) Canceled after 0s
Release Format Lab 0.2.0
2026-09-02 07:16:08 +02:00

108 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown> };
limits: Record<string, number | string | boolean>;
observations: EvidenceObservation[];
provenance: {
execution: "local-browser";
networkRequired: false;
recordedAt: string | null;
};
}
export function createEvidenceReport(
input: Omit<EvidenceReport, "schema" | "contractVersion" | "provenance"> & {
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 11,000 observations.");
const ids = new Set<string>();
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<EvidenceReport>;
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,
});
}