mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 07:46:28 +00:00
Close persisted Evidence, bundle, profile and unknown record shapes so parsing cannot silently remove caller metadata before checking semantic identity. Keep constructor provider projection explicit and JSON payload fields open. Exercise actual MCP export/import with independent record and manifest mutations, unchanged state after rejection, and a valid round trip. Prune duplicate source assertions. Validation: focused domain/CLI/MCP/filesystem checks, converted JSON Schemas, docs and typecheck/lint.
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import canonicalize from "canonicalize";
|
|
import { z } from "zod";
|
|
|
|
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<typeof committedProviderSchema>;
|
|
export type AnalysisProfileCommitment = z.infer<typeof analysisProfileSchema>;
|
|
|
|
/** Create a validated RFC 8785 profile commitment at a provider boundary. */
|
|
export const createAnalysisProfile = (
|
|
provider: CommittedProviderIdentity,
|
|
parameters: Readonly<Record<string, JsonValue>>,
|
|
): 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<typeof unsignedAnalysisProfileSchema>,
|
|
): string => createHash("sha256").update(canonicalJson(profile)).digest("hex");
|
|
|
|
const canonicalJson = (value: JsonValue): string => {
|
|
const encoded = canonicalize(value);
|
|
if (encoded === undefined)
|
|
throw new TypeError("RFC 8785 canonicalization rejected analysis profile");
|
|
return encoded;
|
|
};
|