Files
stack/packages/quality-rails/src/evaluator/digest.ts
T

46 lines
1.7 KiB
TypeScript

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<string, unknown>;
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);
}