Release Format Lab 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 07:16:08 +02:00
parent cfc525a88e
commit c40a2c4b2a
29 changed files with 972 additions and 64 deletions
+107
View File
@@ -0,0 +1,107 @@
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,
});
}