/*
RFC-0707: I/O layer for Nachweis kernel module — R2 upload, SHA-256 hashing, record ID generation.
- Does not implement command handlers — those live in nachweis-*.ts files.
- Does not implement multipart uploads — individual files are under the 5 MB threshold.
- RFC-0890: add resolveNachweisRawScreenshotR2Path, resolveNachweisRawScreenshotLocalPath, parseCaptureXFilename, detectImageMetadata, NachweisScreenshotIngestResult.
- RFC-0891: add resolveNachweisScreenshotDisplayR2Path, downloadFromR2, NachweisScreenshotProcessResult.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- RFC-1307: step 1 — shared PBP layout map, canonical writer, typed onboarding state
RFC-0707, RFC-0713, RFC-0714, RFC-0872, RFC-0873, RFC-0886, RFC-1279
*/
import path from "node:path";
import { parse as yamlParse } from "yaml";
import { z } from "zod";
import { byteHashFile } from "@warpgogol/werkstatt-engine/fingerprint";
import { loadSystemManifest } from "@warpgogol/werkstatt-shared/content";
import { createR2Client, resolveR2ConfigFromEnv, MissingEnvError } from "../evidence/r2-client.ts";
import { resolveCacheClonePath, resolveActiveWorkpieceDir } from "../sternsystem/registry-io.ts";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import { pbpEntityDir } from "@warpgogol/werkstatt-shared/onboarding/pbp-layout";
const NACHWEIS_BUCKET = "nachweis";
const SHA256_HEX_RE = /^[a-f0-9]{64}$/;
export function isValidSha256Hex(value: unknown): value is string {
return typeof value === "string" && SHA256_HEX_RE.test(value);
}
export interface NachweisRecord {
recordId: string;
slug: string;
recordType: string;
titleDe: string;
titleUk: string;
titleEn?: string;
qualityStatus: string;
sourceSha256: string;
r2Path: string;
version: number;
status: "preview" | "published" | "withdrawn";
}
export interface NachweisIngestResult {
recordId: string;
systemId: string;
sourceSha256: string;
r2Path: string;
version: number;
dryRun: boolean;
bordbuchEventId: string | null;
}
export interface NachweisManifestEntry {
recordId: string;
slug: string;
recordType: string;
titleDe: string;
titleUk: string;
titleEn?: string;
qualityStatus: string;
sourceSha256: string;
publishedAt: string | null;
timestampAssurance: "rfc3161" | "eidas-qualified";
// RFC-0872: observation identity fields for technical assessments
kind?: string;
seriesId?: string;
observationId?: string;
observedAt?: string;
assessmentProviderId?: string;
// RFC-0886: display and website fields for Nachweis evidence kinds
display?: { document: string; screenshot: string; websiteLink: string };
websiteUrl?: string;
}
export interface NachweisManifest {
schemaVersion: string;
generatedAt: null;
expiresAt: null;
records: NachweisManifestEntry[];
}
// RFC-0872: Policy-driven publication gate V2
export type NachweisPublicationPolicyId =
"attestation-v1" | "operational-measurement-v1" | "technical-assessment-v1";
export type GateStatus = "pass" | "fail" | "not_applicable";
// RFC-0872: Gate condition IDs
export const GATE_CONDITION_IDS = [
"source-integrity-verified",
"record-approved",
"n3-met",
"legal-content-check-passed",
"consent-granted",
"public-derivative-ready",
"canonical-raw-artifact-verified",
"assessment-metadata-valid",
"execution-authorization-basis-present",
// RFC-0886: display↔consent consistency
"display-consent-consistent",
] as const;
export type GateConditionId = (typeof GATE_CONDITION_IDS)[number];
export interface NachweisGateConditionResult {
id: GateConditionId;
required: boolean;
status: GateStatus;
}
export interface NachweisPublicationGateV2 {
slug: string;
policyId: NachweisPublicationPolicyId;
conditions: NachweisGateConditionResult[];
allPassed: boolean;
}
export class UnsupportedNachweisKindError extends Error {
readonly kind: string;
constructor(kind: string) {
super(`[nachweis] unsupported evidence kind for publication policy: '${kind}'`);
this.name = "UnsupportedNachweisKindError";
this.kind = kind;
}
}
const NACHWEIS_KIND_TO_POLICY: Record = {
"client-statement": "attestation-v1",
"project-confirmation": "attestation-v1",
certificate: "attestation-v1",
"operational-evidence": "operational-measurement-v1",
"technical-assessment": "technical-assessment-v1",
};
export function resolveNachweisPublicationPolicy(kind: string): NachweisPublicationPolicyId {
const policy = NACHWEIS_KIND_TO_POLICY[kind];
if (!policy) {
throw new UnsupportedNachweisKindError(kind);
}
return policy;
}
// RFC-0872: Required conditions per policy
const REQUIRED_CONDITIONS: Record> = {
"attestation-v1": new Set([
"source-integrity-verified",
"record-approved",
"n3-met",
"legal-content-check-passed",
"consent-granted",
"public-derivative-ready",
// RFC-0886: display↔consent consistency (attestation-v1 only)
"display-consent-consistent",
]),
"operational-measurement-v1": new Set([
"source-integrity-verified",
"record-approved",
"n3-met",
"legal-content-check-passed",
"canonical-raw-artifact-verified",
"execution-authorization-basis-present",
]),
"technical-assessment-v1": new Set([
"source-integrity-verified",
"record-approved",
"n3-met",
"legal-content-check-passed",
"canonical-raw-artifact-verified",
"assessment-metadata-valid",
"execution-authorization-basis-present",
]),
};
export function isConditionRequired(
policyId: NachweisPublicationPolicyId,
conditionId: GateConditionId,
): boolean {
return REQUIRED_CONDITIONS[policyId].has(conditionId);
}
// RFC-0872: Shared gate evaluation — used by both nachweis.validate and nachweis.publish
const ISO_8601_WITH_TZ = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
export function validateAssessmentMetadata(assessment: Record): boolean {
if (assessment.profile !== "technical-assessment") return false;
if (typeof assessment.seriesId !== "string" || assessment.seriesId === "") return false;
if (typeof assessment.observationId !== "string" || assessment.observationId === "") return false;
if (typeof assessment.observedAt !== "string" || !ISO_8601_WITH_TZ.test(assessment.observedAt))
return false;
const methodology = assessment.methodology as Record | undefined;
if (!methodology) return false;
if (typeof methodology.runCount !== "number" || methodology.runCount < 1) return false;
const freshness = assessment.freshness as Record | undefined;
if (!freshness) return false;
if (typeof freshness.maxAgeDays !== "number" || freshness.maxAgeDays < 1) return false;
const dimensions = assessment.dimensions;
if (!Array.isArray(dimensions) || dimensions.length < 1) return false;
for (const dim of dimensions as Record[]) {
// score, when present, is finite and in [0,100]
if (dim.score != null) {
if (
typeof dim.score !== "number" ||
!Number.isFinite(dim.score) ||
dim.score < 0 ||
dim.score > 100
)
return false;
}
// numerator/denominator must occur as a valid pair
if (dim.numerator != null || dim.denominator != null) {
if (typeof dim.numerator !== "number" || typeof dim.denominator !== "number") return false;
if (dim.denominator <= 0) return false;
if (dim.numerator < 0 || dim.numerator > dim.denominator) return false;
}
// samples, when present, contain finite values in [0,100]
if (dim.samples != null) {
if (!Array.isArray(dim.samples)) return false;
for (const s of dim.samples) {
if (typeof s !== "number" || !Number.isFinite(s) || s < 0 || s > 100) return false;
}
// min/max must match sample extrema when samples are present
const sampleMin = Math.min(...(dim.samples as number[]));
const sampleMax = Math.max(...(dim.samples as number[]));
if (dim.min != null && dim.min !== sampleMin) return false;
if (dim.max != null && dim.max !== sampleMax) return false;
}
}
// providerReportUrl, when present, must be HTTPS
if (assessment.providerReportUrl != null) {
if (
typeof assessment.providerReportUrl !== "string" ||
!assessment.providerReportUrl.startsWith("https:")
)
return false;
}
return true;
}
export interface NachweisGateInput {
evidenceData: Record;
consentData: Record | undefined;
bordbuchEntries: {
kind: string;
metadata?: Record | null;
summary: string;
}[];
}
export function evaluateGateV2(
slug: string,
kind: string,
input: NachweisGateInput,
): NachweisPublicationGateV2 {
const policyId = resolveNachweisPublicationPolicy(kind);
const items = input.evidenceData.items as
| Record
| undefined;
const assessment = input.evidenceData.assessment as Record | undefined;
// RFC-0886: per-aspect consent logic — all visible display aspects must have granted consent
const display = input.evidenceData.display as Record | undefined;
const consentScope = input.consentData?.consentScope as
Record | undefined;
const aspects = ["document", "screenshot", "websiteLink"];
const visibleAspects = aspects.filter((a) => display?.[a] === "visible");
const consentGranted = visibleAspects.every((a) => consentScope?.[a]?.status === "granted");
const displayConsentConsistent = consentGranted;
const sourceIntegrityVerified =
items != null &&
Object.keys(items).length > 0 &&
Object.values(items).every((item) => isValidSha256Hex(item.sha256));
// RFC-1279: bordbuch-derived conditions count only the evaluated record's own
// entries — an entry without metadata.slug satisfies no condition (fail-closed).
const scopedBordbuchEntries = input.bordbuchEntries.filter((e) => e.metadata?.slug === slug);
const recordApproved = scopedBordbuchEntries.some(
(e) => e.kind === "nachweis-record" && e.metadata?.approved === true,
);
const verificationLevelMet = scopedBordbuchEntries.some(
(e) => e.kind === "nachweis-record" && e.metadata?.verificationLevel === "N3",
);
const publicDerivativeReady =
items != null && Object.values(items).some((item) => item.storage === "public");
const legalContentCheckPassed = scopedBordbuchEntries.some(
(e) => e.kind === "nachweis-record" && e.metadata?.legalContentCheckPassed === true,
);
const canonicalRawArtifactVerified =
items != null &&
Object.values(items).some(
(item) =>
item.role === "raw-result" && item.canonical === true && isValidSha256Hex(item.sha256),
);
const assessmentMetadataValid = assessment != null && validateAssessmentMetadata(assessment);
const executionAuthorizationBasisPresent =
assessment != null &&
assessment.authorizationBasis != null &&
assessment.authorizationBasis !== "";
const conditionResults: Record = {
"source-integrity-verified": sourceIntegrityVerified,
"record-approved": recordApproved,
"n3-met": verificationLevelMet,
"legal-content-check-passed": legalContentCheckPassed,
"consent-granted": consentGranted,
"public-derivative-ready": publicDerivativeReady,
"canonical-raw-artifact-verified": canonicalRawArtifactVerified,
"assessment-metadata-valid": assessmentMetadataValid,
"execution-authorization-basis-present": executionAuthorizationBasisPresent,
"display-consent-consistent": displayConsentConsistent,
};
const conditions: NachweisGateConditionResult[] = (
Object.keys(conditionResults) as GateConditionId[]
).map((id) => {
const required = isConditionRequired(policyId, id);
const passed = conditionResults[id];
const status = required ? (passed ? "pass" : "fail") : "not_applicable";
return { id, required, status };
});
const allPassed = conditions.every((c) => !c.required || c.status === "pass");
return {
slug,
policyId,
conditions,
allPassed,
};
}
export interface NachweisValidateResult {
systemId: string;
records: number;
violations: NachweisViolation[];
gateResults: NachweisPublicationGateV2[];
}
export interface NachweisViolation {
rule: string;
message: string;
recordId?: string;
severity?: "error" | "warning";
}
export interface NachweisConsentUpdateResult {
consentId: string;
systemId: string;
scope: string;
previousStatus: string;
newStatus: string;
bordbuchEventId: string;
}
// RFC-0886: Screenshot upload result
export interface NachweisScreenshotUploadResult {
slug: string;
systemId: string;
sha256: string;
mediaType: string;
storage: "public";
r2Key: string;
bordbuchEventId: string;
}
export interface NachweisPublishResult {
recordId: string;
systemId: string;
published: boolean;
gateResult: NachweisPublicationGateV2;
bordbuchEventId: string | null;
}
export interface NachweisWithdrawResult {
recordId: string;
systemId: string;
withdrawn: boolean;
alreadyWithdrawn: boolean;
bordbuchEventIds: string[];
}
export interface NachweisApproveResult {
slug: string;
systemId: string;
verificationLevel: string;
legalContentCheckPassed: boolean;
bordbuchEventId: string | null;
}
export interface NachweisPublicDerivativeResult {
slug: string;
systemId: string;
r2Path: string;
publicDerivativeSha256: string;
bordbuchEventId: string | null;
alreadyUploaded: boolean;
}
export async function computeSourceSha256(filePath: string): Promise {
const hash = await byteHashFile(filePath);
return hash;
}
export function generateRecordId(slug: string): string {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
return `nr_${slug}_${date}`;
}
export function resolveNachweisR2Path(systemId: string, recordId: string, version: number): string {
return `${systemId}/private/${recordId}/v${version}/source.pdf`;
}
export function resolveNachweisPublicR2Path(
systemId: string,
recordId: string,
version: number,
): string {
return `${systemId}/public/${recordId}/v${version}/public.pdf`;
}
// RFC-0886: R2 path for website screenshots — separate path prefix from evidence PDFs
export function resolveNachweisScreenshotR2Path(
systemId: string,
slug: string,
ext: string,
): string {
return `${systemId}/screenshots/${slug}/website-screenshot${ext}`;
}
export async function uploadToR2(
fileBuffer: Uint8Array,
r2Path: string,
contentType?: string,
): Promise {
const config = resolveR2ConfigFromEnv(NACHWEIS_BUCKET, "R2_NACHWEIS");
const client = createR2Client(config);
await client.putObject({
key: r2Path,
body: fileBuffer,
contentType: contentType ?? "application/pdf",
});
}
// RFC-0891: Download a file from R2 private storage (used for raw screenshot fallback)
export async function downloadFromR2(r2Path: string): Promise {
const config = resolveR2ConfigFromEnv(NACHWEIS_BUCKET, "R2_NACHWEIS");
const client = createR2Client(config);
const result = await client.getObject(r2Path);
return result.body;
}
// RFC-0891: R2 path for processed display screenshots — always .webp
export function resolveNachweisScreenshotDisplayR2Path(systemId: string, slug: string): string {
return `${systemId}/screenshots/${slug}/website-screenshot.webp`;
}
// RFC-0891: Result of nachweis.screenshot.process command
export interface NachweisScreenshotProcessResult {
slug: string;
systemId: string;
rawSha256: string;
rawDimensions: { width: number; height: number };
cropRegion: { left: number; top: number; width: number; height: number };
displaySha256: string;
displayMediaType: string;
displayWidth: number;
displayHeight: number;
r2Key: string;
capturedAt: string | null;
bordbuchEventId: string;
}
export function isMissingEnvError(err: unknown): err is MissingEnvError {
return err instanceof MissingEnvError;
}
export function resolvePbpEntityDir(cachePath: string, lang: string, entityType: string): string {
const subDir = pbpEntityDir(entityType);
return path.join(cachePath, "src", "content", "business-profile", lang, subDir);
}
export async function resolveNachweisCachePath(
workspaceRoot: string,
systemId: string,
): Promise {
// Workpiece-aware: during an active mission, read from the workpiece directory
// so that agents can fix data issues and re-run validate without needing to
// reconcile first (which itself requires validate to pass — circular dependency).
const workpieceDir = await resolveActiveWorkpieceDir(workspaceRoot, systemId);
if (workpieceDir) return workpieceDir;
return resolveCacheClonePath(workspaceRoot, systemId);
}
export async function resolveDefaultLang(cachePath: string): Promise {
const contentDir = path.join(cachePath, "src", "content");
const { manifest } = await loadSystemManifest(contentDir);
const i18n = manifest.i18n as { default?: string } | undefined;
if (!i18n?.default) {
throw new Error(
"[nachweis] system.md i18n.default is required to resolve PBP entity language.",
);
}
return i18n.default;
}
export async function readEntitledFeaturesFromCache(
cachePath: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const entitlementsPath = path.join(cachePath, "src", "entitlements.generated.yaml");
if (!(await io.exists(entitlementsPath))) {
return null;
}
try {
const raw = await io.readFile(entitlementsPath);
const parsed = yamlParse(raw) as { features?: unknown };
return Array.isArray(parsed.features) ? parsed.features.map(String) : null;
} catch {
return null;
}
}
export async function isNachweisEntitled(
workspaceRoot: string,
systemId: string,
): Promise {
const cachePath = await resolveNachweisCachePath(workspaceRoot, systemId);
const features = await readEntitledFeaturesFromCache(cachePath);
return features?.includes("nachweis") ?? false;
}
export function makeSkipResult(
commandName: string,
systemId: string,
): {
data: Record;
exitCode: 0;
summary: string;
} {
return {
data: { systemId, skipped: true, reason: "nachweis entitlement not resolved" },
exitCode: 0,
summary: `[${commandName}] skipped — nachweis entitlement not resolved for ${systemId}`,
};
}
// RFC-0890: Raw screenshot ingestion result
export interface NachweisScreenshotIngestResult {
slug: string;
systemId: string;
sha256: string;
mediaType: string;
originalFilename: string;
width: number;
height: number;
capturedAt: string | null;
r2Key: string;
localPath: string;
bordbuchEventId: string;
alreadyIngested: boolean;
}
// RFC-0890: R2 path for raw screenshots — private storage, separate from display variant
export function resolveNachweisRawScreenshotR2Path(
systemId: string,
slug: string,
originalFilename: string,
): string {
return `${systemId}/screenshots/${slug}/raw/${originalFilename}`;
}
// RFC-0890: Local cache clone path for raw screenshots
export function resolveNachweisRawScreenshotLocalPath(
cachePath: string,
slug: string,
originalFilename: string,
): string {
return path.join(cachePath, "trust", "evidence", "screenshots", slug, "raw", originalFilename);
}
// RFC-0890: Parse CaptureX filename pattern to extract capturedAt
// Pattern: CaptureX_YYYY-MM-DD_HHMMSS_domain.ext
const CAPTUREX_REGEX =
/^CaptureX_(\d{4})-(\d{2})-(\d{2})_(\d{2})(\d{2})(\d{2})_(.+)\.([a-zA-Z0-9]+)$/;
export function parseCaptureXFilename(filename: string): { capturedAt: string } | null {
const match = filename.match(CAPTUREX_REGEX);
if (!match) return null;
const [, year, month, day, hour, minute, second] = match;
return {
capturedAt: `${year}-${month}-${day}T${hour}:${minute}:${second}Z`,
};
}
// RFC-0890: Detect image metadata (mediaType, width, height) from file content via sharp
// Uses dynamic import to avoid static dependency on sharp in stack-agnostic werkstatt package
const SHARP_FORMAT_TO_MEDIA_TYPE: Record = {
jpeg: "image/jpeg",
png: "image/png",
webp: "image/webp",
gif: "image/gif",
tiff: "image/tiff",
avif: "image/avif",
svg: "image/svg+xml",
};
export async function detectImageMetadata(
filePath: string,
): Promise<{ mediaType: string; width: number; height: number }> {
const sharp = (await import("sharp")).default;
const metadata = await sharp(filePath).metadata();
const mediaType = SHARP_FORMAT_TO_MEDIA_TYPE[metadata.format ?? ""] ?? "application/octet-stream";
if (!metadata.width || !metadata.height) {
throw new Error(
`[nachweis] could not read image dimensions from ${filePath} (format: ${metadata.format ?? "unknown"})`,
);
}
return { mediaType, width: metadata.width, height: metadata.height };
}
// RFC-0873: Assessment bundle types and helpers
export interface AssessmentBundleArtifact {
key: string;
role: "raw-result" | "report" | "screenshot" | "summary" | "methodology";
file: string;
mediaType: string;
canonical: boolean;
}
export interface AssessmentBundleV1 {
schemaVersion: "nachweis-assessment-bundle@1";
systemId: string;
slug: string;
title: Record;
seriesId: string;
observationId: string;
subject: { url: string; canonicalUrl?: string };
provider: { id: string; name: string; homepage?: string };
tool: { id: string; name: string; version?: string };
execution: {
mode: "operator-run" | "provider-run";
authorizationBasis: "site-owner" | "service-contract" | "explicit-operator";
};
observedAt: string;
methodology: {
id: string;
version: string;
runCount: number;
aggregation: "provider" | "median" | "none";
};
result: {
overall?: { score?: number; level?: string };
dimensions: Array<{
id: string;
providerLabel: string;
score?: number;
numerator?: number;
denominator?: number;
status?: "pass" | "fail" | "not-checked";
level?: string;
experimental?: boolean;
min?: number;
max?: number;
samples?: number[];
}>;
};
freshness: { maxAgeDays: number };
providerReportUrl?: string;
artifacts: AssessmentBundleArtifact[];
}
export interface AssessmentIngestResult {
systemId: string;
slug: string;
seriesId: string;
observationId: string;
verificationLevel: "N1";
artifactHashes: Record;
alreadyIngested: boolean;
bordbuchEventId: string | null;
dryRun: boolean;
}
const ISO_8601_WITH_TZ_BUNDLE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
const assessmentDimensionSchema = z
.object({
id: z.string().min(1),
providerLabel: z.string().min(1),
score: z.number().finite().min(0).max(100).optional(),
numerator: z.number().finite().min(0).optional(),
denominator: z.number().finite().min(1).optional(),
status: z.enum(["pass", "fail", "not-checked"]).optional(),
level: z.string().optional(),
experimental: z.boolean().optional(),
min: z.number().finite().optional(),
max: z.number().finite().optional(),
samples: z.array(z.number().finite().min(0).max(100)).optional(),
})
.refine(
(d) => {
if (d.numerator != null || d.denominator != null) {
return d.numerator != null && d.denominator != null && d.numerator <= d.denominator;
}
return true;
},
{ message: "numerator/denominator must appear as a valid pair" },
)
.refine(
(d) => {
if (d.samples != null && d.samples.length > 0) {
const sMin = Math.min(...d.samples);
const sMax = Math.max(...d.samples);
if (d.min != null && d.min !== sMin) return false;
if (d.max != null && d.max !== sMax) return false;
}
return true;
},
{ message: "min/max must match sample extrema when samples are present" },
);
export const assessmentBundleV1Schema = z
.object({
schemaVersion: z.literal("nachweis-assessment-bundle@1"),
systemId: z.string().min(1),
slug: z
.string()
.min(1)
.regex(/^[a-zA-Z0-9_-]+$/, "slug must be path-safe"),
title: z.record(z.string(), z.string()),
seriesId: z
.string()
.min(1)
.regex(/^[a-zA-Z0-9_-]+$/, "seriesId must be path-safe"),
observationId: z
.string()
.min(1)
.regex(/^[a-zA-Z0-9_-]+$/, "observationId must be path-safe"),
subject: z.object({
url: z.string().url(),
canonicalUrl: z.string().url().optional(),
}),
provider: z.object({
id: z.string().min(1),
name: z.string().min(1),
homepage: z.string().url().optional(),
}),
tool: z.object({
id: z.string().min(1),
name: z.string().min(1),
version: z.string().optional(),
}),
execution: z.object({
mode: z.enum(["operator-run", "provider-run"]),
authorizationBasis: z.enum(["site-owner", "service-contract", "explicit-operator"]),
}),
observedAt: z
.string()
.regex(ISO_8601_WITH_TZ_BUNDLE, "observedAt must be ISO 8601 with timezone"),
methodology: z.object({
id: z.string().min(1),
version: z.string().min(1),
runCount: z.number().int().min(1),
aggregation: z.enum(["provider", "median", "none"]),
}),
result: z.object({
overall: z
.object({
score: z.number().finite().min(0).max(100).optional(),
level: z.string().optional(),
})
.optional(),
dimensions: z.array(assessmentDimensionSchema).min(1),
}),
freshness: z.object({
maxAgeDays: z.number().int().min(1),
}),
providerReportUrl: z
.string()
.url()
.refine((u) => u.startsWith("https:"), { message: "providerReportUrl must be HTTPS" })
.optional(),
artifacts: z
.array(
z.object({
key: z
.string()
.min(1)
.regex(/^[a-zA-Z0-9_.-]+$/, "artifact key must be path-safe"),
role: z.enum(["raw-result", "report", "screenshot", "summary", "methodology"]),
file: z.string().min(1),
mediaType: z.string().min(1),
canonical: z.boolean(),
}),
)
.min(1),
})
.refine((b) => b.artifacts.some((a) => a.role === "raw-result" && a.canonical), {
message: "at least one canonical raw-result artifact is required",
});
const MEDIA_TYPE_TO_EXT: Record = {
"application/json": ".json",
"image/png": ".png",
"image/jpeg": ".jpg",
"image/webp": ".webp",
"image/svg+xml": ".svg",
"application/pdf": ".pdf",
"text/html": ".html",
"text/plain": ".txt",
"text/csv": ".csv",
};
export function mediaTypeToExt(mediaType: string): string {
return MEDIA_TYPE_TO_EXT[mediaType] ?? ".bin";
}
export function resolveAssessmentR2Path(
systemId: string,
seriesId: string,
observationId: string,
artifactKey: string,
ext: string,
): string {
return `${systemId}/private/assessments/${seriesId}/${observationId}/${artifactKey}${ext}`;
}