a11oy / packages /policy /policy_loader.ts
betterwithage's picture
sync(space): complete build context — fix BUILD_ERROR (CTO)
518343a verified
Raw
History Blame Contribute Delete
12.4 kB
/**
* policy_loader.ts
* Doctrine v6 R3 — Vertical Governance Receipts
*
* Loads, validates, and semantically checks vertical governance policy YAML files.
* Produces a validated PolicyDocument with an initial receipt chain entry upon success.
*
* Features:
* - JSON Schema validation via AJV (structural)
* - Semantic validation: axis↔label coherence, mandatory axis coverage,
* minimum_lambda_coverage, quorum sanity, vertical↔regime matrix check
* - NFC Unicode normalisation of all string fields before hashing
* - SHA3-256 Merkle DAG receipt generation on successful load
* - Configurable retention/staleness check on effective_date
*
* Citations:
* - Doctrine v6 §4.7 (Merkle DAG p50 ≤ 5 µs)
* - Doctrine v6 §3.2 (re-ID risk threshold)
* - NIST SP 800-185 (SHA3-256)
* - RFC 3629 (UTF-8 / Unicode normalisation)
*/
import Ajv from "ajv";
import addFormats from "ajv-formats";
import * as fs from "fs";
import * as path from "path";
import * as crypto from "crypto";
import * as yaml from "js-yaml";
// ── Type Definitions ──────────────────────────────────────────────────────────
export type LambdaAxis = "Λ1"|"Λ2"|"Λ3"|"Λ4"|"Λ5"|"Λ6"|"Λ7"|"Λ8"|"Λ9"|"Λ10";
export type LambdaLabel = "Transparency"|"Accountability"|"Privacy"|"Fairness"|
"Safety"|"Security"|"Auditability"|"Robustness"|
"Explainability"|"Sovereignty";
export type EnforcementLevel = "mandatory"|"recommended"|"informational";
export interface AxisMapping {
axis: LambdaAxis;
label: LambdaLabel;
weight: number;
enforcement: EnforcementLevel;
rationale?: string;
}
export interface RegulatoryClause {
clause_id: string;
title: string;
citation: string;
full_ref: string;
lambda_axes: AxisMapping[];
}
export interface PolicyMeta {
title: string;
description: string;
authority: string;
receipt_chain_required: boolean;
merkle_root_algorithm: string;
[key: string]: unknown;
}
export interface ComplianceThresholds {
minimum_lambda_coverage: number;
mandatory_axes: LambdaAxis[];
receipt_retention_days: number;
[key: string]: unknown;
}
export interface ReceiptChainConfig {
algorithm: string;
chaining: string;
quorum: string;
nodes: string[];
[key: string]: unknown;
}
export interface PolicyDocument {
schema_version: string;
vertical: string;
regime: string;
effective_date: string;
jurisdiction: string;
meta: PolicyMeta;
regulatory_clauses: RegulatoryClause[];
compliance_thresholds: ComplianceThresholds;
receipt_chain: ReceiptChainConfig;
}
export interface LoadResult {
ok: boolean;
policy?: PolicyDocument;
receipt?: PolicyLoadReceipt;
errors?: string[];
warnings?: string[];
}
export interface PolicyLoadReceipt {
receipt_id: string;
event_type: "POLICY_LOAD";
timestamp_iso8601: string;
loader_version: string;
policy_file_hash: string; // SHA3-256 (simulated as SHA-256) of raw YAML bytes
policy_content_hash: string; // SHA3-256 of normalised JSON
vertical: string;
regime: string;
clauses_count: number;
covered_axes: LambdaAxis[];
mandatory_axes_satisfied: boolean;
prev_receipt_hash: string | null;
merkle_root: string;
}
// ── Axis↔Label Coherence Map (Doctrine v6 §1.2) ──────────────────────────────
const AXIS_LABEL_MAP: Record<LambdaAxis, LambdaLabel> = {
"Λ1": "Transparency",
"Λ2": "Accountability",
"Λ3": "Privacy",
"Λ4": "Fairness",
"Λ5": "Safety",
"Λ6": "Security",
"Λ7": "Auditability",
"Λ8": "Robustness",
"Λ9": "Explainability",
"Λ10": "Sovereignty",
};
// ── Vertical↔Regime Coherence Matrix (Doctrine v6 §2.3) ──────────────────────
const VERTICAL_REGIME_PREFIXES: Record<string, string[]> = {
healthcare: ["HIPAA", "HITECH", "21-CFR"],
financial: ["SOX", "Dodd-Frank", "SR11-7", "FINRA", "ECOA"],
defense: ["CMMC", "NIST", "DFARS", "ITAR"],
aviation: ["DO-178", "DO-333", "FAA", "EASA", "ARINC"],
automotive: ["ISO-26262", "ISO-21448", "UL-4600", "SAE", "UNECE"],
pharmaceutical:["21-CFR", "FDA", "ICH", "EU-AI-Act"],
energy: ["NERC-CIP", "FERC", "DOE"],
maritime: ["IMO-ISPS", "SOLAS", "IMO", "MSC-FAL", "MARPOL", "COLREGS"],
legaltech: ["GDPR", "EU-AI-Act", "eIDAS"],
academic: ["Common-Rule", "Belmont", "COPE", "NIH", "NSF", "EU-AI-Act"],
};
// ── Internal Helpers ──────────────────────────────────────────────────────────
/** NFC-normalise all string values in an object (deep) — RFC 3629 §7 */
function normaliseStrings(obj: unknown): unknown {
if (typeof obj === "string") return obj.normalize("NFC");
if (Array.isArray(obj)) return obj.map(normaliseStrings);
if (obj !== null && typeof obj === "object") {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
result[k] = normaliseStrings(v);
}
return result;
}
return obj;
}
/** SHA-256 of a UTF-8 string (production: replace with SHA3-256 per NIST SP 800-185) */
function sha256hex(data: string): string {
return crypto.createHash("sha256").update(data, "utf8").digest("hex");
}
/** Compute covered Λ-axes from all clauses */
function coveredAxes(clauses: RegulatoryClause[]): Set<LambdaAxis> {
const axes = new Set<LambdaAxis>();
for (const c of clauses) {
for (const m of c.lambda_axes) axes.add(m.axis);
}
return axes;
}
// ── AJV Validator (singleton) ─────────────────────────────────────────────────
let _validator: Ajv.ValidateFunction | null = null;
function getValidator(schemaPath: string): Ajv.ValidateFunction {
if (_validator) return _validator;
const ajv = new Ajv({ allErrors: true, strict: false, coerceTypes: false });
addFormats(ajv);
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
const validator = ajv.compile(schema);
_validator = validator;
return validator;
}
// ── Semantic Validators ───────────────────────────────────────────────────────
function validateAxisLabelCoherence(policy: PolicyDocument): string[] {
const errors: string[] = [];
for (const clause of policy.regulatory_clauses) {
for (const m of clause.lambda_axes) {
const expected = AXIS_LABEL_MAP[m.axis];
if (expected && m.label !== expected) {
errors.push(
`[${clause.clause_id}] Axis ${m.axis} has label '${m.label}', expected '${expected}'`
);
}
}
}
return errors;
}
function validateMandatoryAxesCovered(policy: PolicyDocument): string[] {
const errors: string[] = [];
const covered = coveredAxes(policy.regulatory_clauses);
for (const ax of policy.compliance_thresholds.mandatory_axes) {
if (!covered.has(ax)) {
errors.push(`Mandatory axis ${ax} not covered by any regulatory clause`);
}
}
return errors;
}
function validateCoverageMinimum(policy: PolicyDocument): string[] {
const covered = coveredAxes(policy.regulatory_clauses);
const min = policy.compliance_thresholds.minimum_lambda_coverage;
if (covered.size < min) {
return [`Covered axis count (${covered.size}) < minimum_lambda_coverage (${min})`];
}
return [];
}
function validateQuorum(policy: PolicyDocument): string[] {
const errors: string[] = [];
const { quorum, nodes } = policy.receipt_chain;
const match = quorum.match(/^(\d+)-of-(\d+)$/);
if (match) {
const [, n, d] = match.map(Number);
if (n > d) errors.push(`Impossible quorum: ${n}-of-${d} (numerator > denominator)`);
if (n > nodes.length) errors.push(`Quorum requires ${n} signers but only ${nodes.length} nodes defined`);
}
return errors;
}
function validateVerticalRegimeCoherence(policy: PolicyDocument): string[] {
const warnings: string[] = [];
const prefixes = VERTICAL_REGIME_PREFIXES[policy.vertical] ?? [];
const regimeMatches = prefixes.some((p) => policy.regime.includes(p));
if (!regimeMatches) {
warnings.push(
`vertical '${policy.vertical}' regime '${policy.regime}' not in expected regime list [${prefixes.join(", ")}]. Review required.`
);
}
return warnings;
}
function validateEffectiveDateRecency(policy: PolicyDocument): string[] {
const warnings: string[] = [];
const effectiveMs = new Date(policy.effective_date).getTime();
const nowMs = Date.now();
const tenYearsMs = 10 * 365.25 * 24 * 3600 * 1000;
if (nowMs - effectiveMs > tenYearsMs) {
warnings.push(`effective_date '${policy.effective_date}' is more than 10 years in the past. Policy may be stale.`);
}
return warnings;
}
// ── Main Load Function ────────────────────────────────────────────────────────
/**
* Load and validate a vertical governance policy YAML file.
*
* @param filepath Absolute or relative path to the .yaml policy file
* @param schemaPath Path to a11oy-knowledge.schema.json
* @param prevReceiptHash Hash of the preceding receipt (null for first load)
* @returns LoadResult with policy, receipt, errors, and warnings
*/
export function loadVerticalPolicy(
filepath: string,
schemaPath: string,
prevReceiptHash: string | null = null
): LoadResult {
const errors: string[] = [];
const warnings: string[] = [];
// 1. Read file
let rawYaml: string;
try {
rawYaml = fs.readFileSync(filepath, "utf8");
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return { ok: false, errors: [`Cannot read file '${filepath}': ${msg}`] };
}
// 2. Parse YAML
let rawObject: unknown;
try {
rawObject = yaml.load(rawYaml);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return { ok: false, errors: [`YAML parse error in '${filepath}': ${msg}`] };
}
// 3. NFC normalise all strings (RFC 3629)
const normObject = normaliseStrings(rawObject);
// 4. JSON Schema validation (structural)
const validate = getValidator(schemaPath);
const schemaValid = validate(normObject);
if (!schemaValid) {
const schemaErrors = (validate.errors ?? []).map(
(e) => `[schema] ${e.instancePath || "/"} ${e.message}`
);
return { ok: false, errors: schemaErrors };
}
const policy = normObject as unknown as PolicyDocument;
// 5. Semantic validations
errors.push(...validateAxisLabelCoherence(policy));
errors.push(...validateMandatoryAxesCovered(policy));
errors.push(...validateCoverageMinimum(policy));
errors.push(...validateQuorum(policy));
warnings.push(...validateVerticalRegimeCoherence(policy));
warnings.push(...validateEffectiveDateRecency(policy));
if (errors.length > 0) {
return { ok: false, errors, warnings };
}
// 6. Compute receipt
const fileHash = sha256hex(rawYaml);
const contentHash = sha256hex(JSON.stringify(policy));
const covered = Array.from(coveredAxes(policy.regulatory_clauses)).sort() as LambdaAxis[];
const mandatory = policy.compliance_thresholds.mandatory_axes;
const coveredSet = new Set(covered);
const mandatorySatisfied = mandatory.every((ax) => coveredSet.has(ax));
const receiptPartial = {
receipt_id: `rl-${sha256hex(filepath + contentHash).slice(0, 16)}`,
event_type: "POLICY_LOAD" as const,
timestamp_iso8601: new Date().toISOString(),
loader_version: "1.0.0",
policy_file_hash: fileHash,
policy_content_hash: contentHash,
vertical: policy.vertical,
regime: policy.regime,
clauses_count: policy.regulatory_clauses.length,
covered_axes: covered,
mandatory_axes_satisfied: mandatorySatisfied,
prev_receipt_hash: prevReceiptHash,
};
const merkle_root = sha256hex(JSON.stringify(receiptPartial));
const receipt: PolicyLoadReceipt = { ...receiptPartial, merkle_root };
return { ok: true, policy, receipt, warnings };
}