Guides
Audit trail
A decision is reproducible: the same contract and facts always give the same verdict and clause. Record enough to reproduce it, and every decision can be checked later.
What to record
| Field | Why |
|---|---|
contract_sha256 | identifies the exact contract, by its canonical text |
facts_sha256 | identifies the facts, hashed over canonical JSON |
action, verdict, clause_id | the decision itself |
at | when; supplied by the application, never by the contract |
Keep the facts themselves wherever your retention rules allow: the hash proves they were not altered.
src/audit.tsexample project// Decision records you can replay: which contract decided what, from which facts.
import { createHash } from 'node:crypto';
import { contractDigest, parse } from '../vendor/ulang-lite/src/index.ts';
import { loadPolicy } from './policy.ts';
export interface DecisionRecord {
readonly at: string;
readonly action: string;
readonly verdict: 'PERMIT' | 'DENY';
readonly clause_id: string;
readonly contract: string;
readonly contract_sha256: string;
readonly facts_sha256: string;
}
/** JSON with keys sorted at every level, so equal facts always hash the same. */
export function canonical(value: unknown): string {
if (value === null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
const object = value as Record<string, unknown>;
return `{${Object.keys(object).sort().map((k) => `${JSON.stringify(k)}:${canonical(object[k])}`).join(',')}}`;
}
const sha256 = (text: string) => createHash('sha256').update(text).digest('hex');
export function record(policy: ReturnType<typeof loadPolicy>, action: string, facts: unknown, at: string): DecisionRecord {
const outcome = policy.decide(action, facts);
if ('error' in outcome) throw new Error(outcome.error);
return {
at, action,
verdict: outcome.verdict, clause_id: outcome.clause_id,
contract: policy.name, contract_sha256: outcome.contract_sha256,
facts_sha256: sha256(canonical(facts)),
};
}
/** Re-decide a record from the stored facts and a contract source; true only if everything matches. */
export function replay(entry: DecisionRecord, facts: unknown, source: string): boolean {
const contract = parse(source).contracts.find((c) => c.name === entry.contract);
if (!contract || contractDigest(contract) !== entry.contract_sha256) return false;
if (sha256(canonical(facts)) !== entry.facts_sha256) return false;
const again = record(loadPolicy(source), entry.action, facts, entry.at);
return again.verdict === entry.verdict && again.clause_id === entry.clause_id;
}scripts/audit-demo.tsexample project// Record two decisions, then replay them: once against the same contract, once against an edited one.
import { readFileSync } from 'node:fs';
import { loadPolicy } from '../src/policy.ts';
import { record, replay } from '../src/audit.ts';
const source = readFileSync(new URL('../policy/deploy.ul', import.meta.url), 'utf8');
const policy = loadPolicy(source, 'policy/deploy.ul');
const facts = JSON.parse(readFileSync(new URL('../facts/green.json', import.meta.url), 'utf8'));
const frozen = JSON.parse(readFileSync(new URL('../facts/freeze.json', import.meta.url), 'utf8'));
const log = [
{ entry: record(policy, 'deploy', facts, '2026-10-15T09:30:00Z'), facts },
{ entry: record(policy, 'deploy', frozen, '2026-10-15T09:31:00Z'), facts: frozen },
];
for (const { entry } of log) console.log(JSON.stringify(entry));
const edited = source.replace('min_approvals: int == 2', 'min_approvals: int == 1');
for (const { entry, facts } of log) {
console.log(`${entry.at} replay: same contract ${replay(entry, facts, source)}, edited contract ${replay(entry, facts, edited)}`);
}$ node scripts/audit-demo.ts
{"at":"2026-10-15T09:30:00Z","action":"deploy","verdict":"PERMIT","clause_id":"ProductionDeploy.deploy.permit","contract":"ProductionDeploy","contract_sha256":"686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059","facts_sha256":"8f0d1a7feaa98af888484dbce8207f5aff537b889f082cc6b39e2f02dc9d5aa1"}
{"at":"2026-10-15T09:31:00Z","action":"deploy","verdict":"DENY","clause_id":"ProductionDeploy.change_freeze","contract":"ProductionDeploy","contract_sha256":"686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059","facts_sha256":"033610318d2318b48fcb1f796e4521e4f868ef40a846056971908f2b3cc83149"}
2026-10-15T09:30:00Z replay: same contract true, edited contract false
2026-10-15T09:31:00Z replay: same contract true, edited contract falseReplaying against the same contract succeeds. Replaying against a contract with one constant changed fails because the digest no longer matches, even though for these particular facts the verdict would be the same.