import { z } from "zod"; import { digestCanonicalValue } from "./canonicalDigest.js"; import { jsonObjectSchema, type JsonValue } from "./jsonValue.js"; import { digestSchema } from "./../domain/digests.js"; /** Concrete provider identity committed by an analysis profile. */ export const committedProviderSchema = z.strictObject({ id: z.string().min(1), name: z.string().min(1), version: z.string().min(1), }); const unsignedAnalysisProfileSchema = z.strictObject({ provider: committedProviderSchema, parameters: jsonObjectSchema, }); /** Canonical, provider-neutral commitment to analysis-affecting semantics. */ export const analysisProfileSchema = unsignedAnalysisProfileSchema .extend({ digest: digestSchema }) .superRefine((profile, context) => { const { digest: _digest, ...unsigned } = profile; if (profileDigest(unsigned) !== profile.digest) context.addIssue({ code: "custom", path: ["digest"], message: "Analysis profile digest does not match its parameters", }); }); export type CommittedProviderIdentity = z.infer; export type AnalysisProfileCommitment = z.infer; /** Create a validated RFC 8785 profile commitment at a provider boundary. */ export const createAnalysisProfile = ( provider: CommittedProviderIdentity, parameters: Readonly>, ): AnalysisProfileCommitment => { const unsigned = unsignedAnalysisProfileSchema.parse({ provider: { id: provider.id, name: provider.name, version: provider.version, }, parameters, }); return analysisProfileSchema.parse({ ...unsigned, digest: profileDigest(unsigned), }); }; /** Compare two already-validated commitments without interpreting parameters. */ export const analysisProfilesEqual = ( left: AnalysisProfileCommitment, right: AnalysisProfileCommitment, ): boolean => left.digest === right.digest && left.provider.id === right.provider.id && left.provider.name === right.provider.name && left.provider.version === right.provider.version; const profileDigest = ( profile: z.infer, ): string => digestCanonicalValue(profile, "Analysis profile");