import { createHash } from 'node:crypto'; import type { CheckDefinitionSpec, CheckSetPolicySpec } from './types.js'; // Deterministic JSON: object keys sorted at every level so two specs with the // same content always produce the same bytes (and thus the same digest). export function canonicalJson(value: unknown): string { if (value === null || typeof value !== 'object') { return JSON.stringify(value); } if (Array.isArray(value)) { return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`; } const record = value as Record; const keys = Object.keys(record).sort(); return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`; } /** sha256 over the canonical JSON of `value`. */ export function digestContent(value: unknown): string { return createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex'); } /** * Content digest of a check definition: covers the declarative spec (id, * version, canonical check, description, applicability, params) — everything a * reviewer reasons about — while excluding the executable function object. * Changing any covered field changes the digest, so a recorded digest always * identifies exactly which definition content produced a verdict. */ export function digestOfSpec(spec: CheckDefinitionSpec): string { return digestContent({ id: spec.id, version: spec.version, canonicalCheck: spec.canonicalCheck, description: spec.description, appliesTo: spec.appliesTo, params: spec.params, }); } /** Content digest of the per-subject check-set policy. */ export function digestOfPolicy(spec: CheckSetPolicySpec): string { return digestContent(spec); }