University Policy Mapping Frameworks
On this page
A policy mapping framework is the subsystem that turns prose rules into predicates a pipeline can evaluate. NIH cost principles, NSF proposal terms, OSHA laboratory standards, and an institution’s own effort-reporting policy all arrive as documents written for human readers; the spending, procurement, and inventory systems that must honor them speak only in typed fields and boolean checks. The gap between those two worlds is where compliance failures live — a sub-recipient threshold misremembered, an equipment cap applied to the wrong agency, a hazardous-material rule enforced months after the chemical was already on a shelf. This guide specifies the framework that closes that gap: it owns which rules are in force, what they evaluate against, and how that evaluation is recorded, and it is anchored to the foundational principles set out in Core Architecture & Policy Mapping for Research Grants.
For university administrators, research compliance officers, Python automation developers, and laboratory managers, the design goal is the same one that governs the rest of this architecture: a single canonical representation of every active rule, keyed by a deterministic idempotency hash, validated before it can influence any downstream decision, and appended to an immutable ledger so that a year later anyone can prove which version of which rule governed a given transaction. The framework feeds the state guards in Grant Lifecycle Architecture Design and is consumed by every record that flows through Automated Ingestion & Data Sync Workflows.
Problem Framing: Policy as Documentation Cannot Scale
The recurring failure mode is treating policy as something staff read rather than something the system executes. A rule lives in a PDF, a few people remember it, a spreadsheet enforces it by convention, and an audit reconstructs intent long after the money moved. With a single award that is survivable. Across hundreds of concurrent awards carrying overlapping NIH, NSF, OSHA, and EPA obligations — each revised on its own cadence — it is not. Two analysts apply two recollections of the same F&A rate; a procurement script never hears that a cost principle changed; the institution discovers a disallowed cost only when the closeout audit surfaces it.
Modeling policy as a versioned, machine-readable rule set solves three problems at once. It gives every integration one authoritative answer to what is allowed right now. It makes the evaluation reproducible — the same transaction checked against the same rule version always yields the same verdict, on the first run and on every replay. And it produces a decision log that is the audit trail rather than a thing reconstructed from it. This guide builds the framework in layers: the policy constraints that bound it, the canonical schema it operates on, the idempotent implementation that compiles and applies rules, the integration contracts with adjacent systems, and the verification and recovery procedures that keep the whole thing defensible.
Policy Constraints: The Regulatory Boundary
The framework does not interpret regulation — it codifies an interpretation that compliance officers have already approved, then enforces it identically every time. Each source rule is reduced to an executable predicate carrying provenance: the citation it derives from, the date it took effect, and the jurisdictional scope it applies to.
- NIH (2 CFR 200 & NIHGPS): direct-cost allowability, Facilities & Administrative (F&A) rate application, and sub-recipient monitoring thresholds compile into transactional validation rules. A charge against an award is checked for allowability before it can be encumbered.
- NSF (PAPPG): budget-justification alignment, cost-sharing prohibitions, and equipment-ownership clauses become automated procurement gates. NSF’s general prohibition on voluntary committed cost sharing, for instance, is a hard predicate, not a reminder.
- OSHA (29 CFR 1910): laboratory safety, hazardous-material tracking, and PPE procurement mandates are cross-referenced against inventory manifests so that a chemical purchase is validated against storage and training constraints at the point of order.
- EPA (40 CFR): environmental-stewardship requirements, waste-disposal tracking, and chemical procurement limits are embedded into lab-level allocation rules and feed the controls in Equipment Calibration & Lab Inventory Tracking.
Two boundary rules keep this layer honest. First, every rule is version-controlled and declarative — a change is a new version with its own effective date, never an in-place edit, so a transaction is always evaluated against the rule that was in force when it occurred. Second, the policy domain owns interpretation, the implementation domain owns the predicate — a developer never silently changes what a rule means, and an officer never edits code; a rule change is a reviewed, dated configuration commit.
Data Schema & Field Mapping
Every source format — agency XML, portal JSON, a finance CSV — is normalized into one canonical rule model before any logic runs. Disparate inputs converge on a single shape so the evaluator only ever sees typed, validated fields. The canonical policy rule carries enough provenance to be auditable and enough structure to be executable:
| Canonical field | Type | Constraint | Source rule |
|---|---|---|---|
policy_id |
str |
unique, stable across versions | internal registry key |
version |
int |
monotonic, ≥ 1 | institutional change control |
agency |
Agency (enum) |
one of NIH, NSF, OSHA, EPA, INSTITUTIONAL |
sponsor / governing body |
citation |
str |
non-empty, e.g. 2 CFR 200.413 |
the regulation being codified |
predicate_type |
PredicateType (enum) |
threshold, prohibition, allowlist, gate |
derived from rule intent |
field_path |
str |
dotted path into the transaction | maps rule to the data it checks |
operator |
str |
one of <=, >=, ==, in, not_in |
comparison semantics |
value |
int | float | str | list |
type must match operator |
the threshold or set |
effective_date |
date |
ISO 8601, UTC | when the rule takes force |
jurisdiction |
str |
award type / lab scope | who the rule binds |
The transformation rules are strict and few. Monetary thresholds are stored as integer cents to avoid floating-point drift on a compliance boundary. Dates are normalized to UTC at ingress so an effective-date comparison never depends on the server’s local clock. Enumerations are closed sets — an unknown agency or predicate_type is rejected at validation rather than silently passed through. The same canonical discipline governs incoming records on the ingestion side via Schema Validation Pipelines.
Implementation
The framework compiles a source rule into a canonical record, validates it with Pydantic, and upserts it idempotently with SQLAlchemy. The idempotency key folds in the policy version so that re-evaluating a rule under a changed version produces a new, distinct ledger entry instead of silently reusing a stale decision, while a re-ingest of unchanged data resolves to a no-op hit.
import hashlib
import json
from datetime import date, datetime, timezone
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
from sqlalchemy.orm import Session
class Agency(str, Enum):
NIH = "NIH"
NSF = "NSF"
OSHA = "OSHA"
EPA = "EPA"
INSTITUTIONAL = "INSTITUTIONAL"
class PredicateType(str, Enum):
THRESHOLD = "threshold"
PROHIBITION = "prohibition"
ALLOWLIST = "allowlist"
GATE = "gate"
class CanonicalPolicyRule(BaseModel):
"""The single shape every source format is normalized into before evaluation."""
policy_id: str = Field(min_length=1)
version: int = Field(ge=1)
agency: Agency
citation: str = Field(min_length=1)
predicate_type: PredicateType
field_path: str = Field(min_length=1)
operator: str
value: Any
effective_date: date
jurisdiction: str = Field(min_length=1)
@field_validator("operator")
@classmethod
def _known_operator(cls, v: str) -> str:
allowed = {"<=", ">=", "==", "in", "not_in"}
if v not in allowed:
raise ValueError(f"unsupported operator {v!r}; allowed: {sorted(allowed)}")
return v
def idempotency_key(self) -> str:
"""Deterministic SHA-256 over identity + version + canonical content."""
canonical = self.model_dump_json() # Pydantic emits stable field order
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()A rule that fails validation is never coerced into the registry; it is routed to a quarantine queue with an explicit reason and the prior version stays authoritative. The upsert itself is keyed on the idempotency hash so replays converge:
def upsert_policy_rule(
session: Session,
payload: dict[str, Any],
quarantine: list[dict[str, Any]],
) -> dict[str, str]:
"""Validate, then idempotently upsert a single policy rule.
Repeated calls with identical input produce identical state and no
duplicate ledger entries (idempotent_hit). Invalid input is quarantined,
never silently dropped.
"""
try:
rule = CanonicalPolicyRule.model_validate(payload)
except ValueError as exc:
quarantine.append({"payload": payload, "reason": f"validation_error: {exc}"})
return {"status": "quarantined", "reason": "validation_error"}
key = rule.idempotency_key()
existing = session.execute(
select(PolicyRuleRow).where(PolicyRuleRow.idempotency_key == key)
).scalar_one_or_none()
if existing is not None:
return {"status": "idempotent_hit", "key": key}
session.add(
PolicyRuleRow(
idempotency_key=key,
policy_id=rule.policy_id,
version=rule.version,
agency=rule.agency.value,
citation=rule.citation,
predicate_type=rule.predicate_type.value,
field_path=rule.field_path,
operator=rule.operator,
value_json=json.dumps(rule.value, sort_keys=True),
effective_date=rule.effective_date.isoformat(),
jurisdiction=rule.jurisdiction,
applied_at=datetime.now(timezone.utc).isoformat(),
)
)
session.add(
PolicyLedgerRow(rule_key=key, event="rule_activated", state_hash=key)
)
session.commit()
return {"status": "activated", "key": key}Evaluating a transaction against the active rule set is the reverse direction — resolve the rules whose effective_date and jurisdiction bind the transaction, apply each predicate, and let any failure block the operation. The detailed evaluation loop and its scheduling are covered in Setting up automated policy compliance checks for university grants, which provides the diagnostic scaffolding to isolate a failing predicate without halting active research.
Integration Points
The framework is a producer of verdicts, not a system of record for the things it governs, so it integrates by contract. Three boundaries matter.
Toward the grant lifecycle. The lifecycle engine asks the framework, before any guarded transition, whether the proposed move is permitted under the active rules. The contract is a small request/response payload:
{
"request": {
"transaction_type": "encumbrance",
"agency": "NIH",
"award_type": "R01",
"fields": { "direct_cost_cents": 4150000, "cost_share_cents": 0 }
},
"response": {
"verdict": "allow",
"evaluated_rules": ["nih-direct-cost-allow-v3", "nih-cost-share-prohibit-v2"],
"policy_versions": { "nih-direct-cost-allow-v3": 3 }
}
}Toward ERP and HR. When a sponsor publishes a new cost principle, the activated rule is pushed to the ERP procurement gate and the HR effort-certification module through event-driven webhooks, with a scheduled reconciliation job as the backstop for any missed event. Every push carries the policy_versions map so downstream systems record which version they enforced.
Toward LIMS and lab inventory. OSHA and EPA predicates are exported to the laboratory inventory system so a hazardous-material purchase is gated at the point of order rather than flagged after delivery; that export feeds Inventory Threshold Tuning. When a primary export route degrades, verdicts are preserved and re-applied through Fallback Routing Protocols, and every transport boundary is scoped and encrypted under Security Boundary Configuration.
Verification & Audit
Because every rule activation and every verdict is appended to an immutable ledger, verifying the framework is a matter of replaying the recorded content and confirming the hash. The audit hash is computed only over canonical, validated fields — never over wall-clock timestamps or mutable status — so a reproducible match proves the rule that governed a transaction has not been altered since.
def verify_rule_integrity(session: Session, key: str) -> bool:
"""Re-derive the idempotency key from stored fields and compare to the ledger."""
row = session.execute(
select(PolicyRuleRow).where(PolicyRuleRow.idempotency_key == key)
).scalar_one()
recomputed = CanonicalPolicyRule(
policy_id=row.policy_id,
version=row.version,
agency=Agency(row.agency),
citation=row.citation,
predicate_type=PredicateType(row.predicate_type),
field_path=row.field_path,
operator=row.operator,
value=json.loads(row.value_json),
effective_date=date.fromisoformat(row.effective_date),
jurisdiction=row.jurisdiction,
).idempotency_key()
return recomputed == keyAn auditor reproduces a compliance decision by pulling the verdict ledger entry, reading the policy_versions it recorded, re-fetching those exact rule versions, and re-running the predicates against the archived transaction. Any divergence between the recomputed and stored hash is itself an audit finding: it means a field that should have been immutable was mutated, or a rule was edited outside the pipeline. Retention follows the longest applicable sponsor requirement — 2 CFR 200 records retention runs at minimum three years past final report, so ledger rows are never hard-deleted within that window.
Failure Modes & Recovery
Recovery never edits the registry directly; every correction flows back through upsert_policy_rule so it is validated and logged like any other change.
| Symptom | Root cause | Idempotent-safe recovery |
|---|---|---|
| Same rule re-activated on every sync | Idempotency key derived from a non-canonical payload (unsorted keys, float dollars, local-time date) | Route inputs through CanonicalPolicyRule first — model_dump_json fixes field order, cents avoid float drift, dates normalize to UTC — then backfill so the stable key is recognized on replay |
| Valid transaction blocked after a policy update | A stricter rule version took effect mid-batch; the transaction predates it | Re-evaluate against the rule version in force at the transaction’s date, not the latest; if genuinely disallowed, route to exception approval — never lower the predicate to force it through |
| Verdict mismatch between ERP and LIMS | One system enforced a stale version because a webhook was missed | Replay the reconciliation job; both systems pull the current policy_versions map and converge — the ledger shows which version each enforced and when |
| Quarantine backlog growing | An upstream source emitting systematically malformed rule exports with no owner | Group quarantine by reason, assign each reason to its compliance owner, fix the source mapping; corrected rules re-enter under fresh keys with no duplication |
Frequently Asked Questions
Why compile policy into predicates instead of keeping rules in a documented procedure?
A documented procedure is enforced by whoever remembers it, which is how two analysts apply two F&A rates and an audit finds the disallowed cost months later. Compiling each approved interpretation into a versioned predicate means every integration gets one authoritative verdict, the evaluation is reproducible on replay, and the decision log is the audit trail rather than something reconstructed afterward.
What goes into the idempotency key, and why fold in the version?
The key is a SHA-256 over the rule’s identity, its version, and its canonical content. Folding the version in means re-evaluating a rule under a changed version produces a new, distinct ledger entry instead of silently reusing a stale decision, while a re-ingest of unchanged data under the same version resolves to an idempotent_hit and writes nothing.
How does the framework avoid blocking a transaction under a rule that did not exist when it occurred?
Every rule carries an effective_date, and evaluation resolves the version in force at the transaction’s date rather than the current latest. A rule published this quarter cannot retroactively disallow a charge from last quarter; the verdict ledger records exactly which version governed each decision.
What happens to a malformed or unknown rule on ingest?
It is written to the quarantine queue with an explicit reason — a Pydantic validation error, an unknown agency, or an unsupported operator — and the prior rule version stays authoritative. It is never auto-corrected. A compliance officer fixes the source and the corrected rule re-enters under a fresh idempotency key through the same code path.
Related
- Core Architecture & Policy Mapping for Research Grants — the parent architecture this framework plugs into.
- Grant Lifecycle Architecture Design — the state guards that query this framework before every transition.
- Security Boundary Configuration — access scoping and encryption over the verdicts and rule exports this framework produces.
- Fallback Routing Protocols — how verdicts are preserved and re-applied when a primary export route degrades.
- Setting up automated policy compliance checks for university grants — the runnable evaluation loop and scheduling that applies these rules to live transactions.
- Automated Ingestion & Data Sync Workflows — the sibling domain that normalizes external records before they reach this framework.