Versioning Rule References in Audit Trails

A compliance result is meaningless without knowing which rule decided it. Pinning every evaluation to an immutable rule version, resolved to a git commit SHA or a semantic version tag rather than a moving branch, is what lets you reproduce a two-year-old verdict and roll back cleanly when an ordinance amendment turns out to contain a drafting error. This guide shows how to resolve, record, and replay versioned rule references so each entry in your audit trail carries the exact logic that produced it.

Prerequisites

Step-by-step

Step 1: Resolve the rule set to an immutable reference

Never record a branch name like main; it points somewhere different tomorrow. Resolve the working rules to a concrete commit SHA at the moment of evaluation. This mirrors the discipline described in automating zoning code version control with git, where rule changes are committed and tagged rather than edited in place.

import subprocess, json, hashlib
from datetime import datetime, timezone

def resolve_rule_version(repo_dir: str) -> dict:
    """Pin the current rule set to an immutable commit SHA and tag."""
    sha = subprocess.check_output(
        ["git", "-C", repo_dir, "rev-parse", "HEAD"], text=True).strip()
    # describe gives the nearest semantic tag, e.g. v3.2.1-0-g9f2c1a7
    tag = subprocess.check_output(
        ["git", "-C", repo_dir, "describe", "--tags", "--always"], text=True).strip()
    dirty = subprocess.check_output(
        ["git", "-C", repo_dir, "status", "--porcelain"], text=True).strip()
    return {"repo": repo_dir, "sha": sha, "tag": tag, "clean": dirty == ""}

Step 2: Hash the rule payload actually loaded

A commit SHA proves what the repository held, but a defensive audit also hashes the concrete rule bytes fed to the engine. If someone bypasses git and hand-edits a file, the payload digest will diverge from the commit, exposing the tampering.

def hash_rule_payload(rule_path: str) -> str:
    with open(rule_path, "rb") as fh:
        return hashlib.sha256(fh.read()).hexdigest()

def build_rule_reference(repo_dir: str, rule_path: str) -> dict:
    ref = resolve_rule_version(repo_dir)
    ref["payload_sha256"] = hash_rule_payload(rule_path)
    ref["resolved_at"] = datetime.now(timezone.utc).isoformat()
    return ref

Step 3: Attach the reference to each evaluation

When the engine scores a parcel, stamp the result with the resolved reference. Because the setback measurement itself must run in a metric CRS, the snippet projects before computing area so the recorded verdict and the recorded rule version describe the same, correct calculation.

import geopandas as gpd

def evaluate_with_reference(parcels: gpd.GeoDataFrame, rule_ref: dict,
                            min_area_m2: float, metric_crs="EPSG:2926") -> gpd.GeoDataFrame:
    projected = parcels.to_crs(metric_crs)          # project BEFORE measuring area
    projected["area_m2"] = projected.geometry.area
    projected["compliant"] = projected["area_m2"] >= min_area_m2
    projected["rule_sha"] = rule_ref["sha"]         # immutable reference on every row
    projected["rule_tag"] = rule_ref["tag"]
    projected["rule_payload_sha256"] = rule_ref["payload_sha256"]
    return projected

Step 4: Write the versioned audit record

Persist the reference alongside the run so it can be queried later. A deterministic JSON dump keeps records diffable and tamper-evident on append-only storage.

def write_audit_record(run_id: str, rule_ref: dict, summary: dict, path: str) -> None:
    record = {
        "run_id": run_id,
        "rule_reference": rule_ref,   # sha + tag + payload digest
        "result_summary": summary,
        "written_at": datetime.now(timezone.utc).isoformat(),
    }
    with open(path, "w", encoding="utf-8") as fh:
        json.dump(record, fh, indent=2, sort_keys=True)

Verification

To prove reproducibility, check out the recorded SHA into a clean workspace, re-run the evaluation, and confirm the payload digest and verdicts match the archived record. A mismatch means either the checkout was dirty or the rule file was altered outside git.

def verify_reproduction(archived: dict, repo_dir: str, rule_path: str) -> None:
    current = build_rule_reference(repo_dir, rule_path)
    assert current["sha"] == archived["rule_reference"]["sha"], "wrong commit checked out"
    assert current["payload_sha256"] == archived["rule_reference"]["payload_sha256"], \
        "rule payload differs from the archived audit record"
    print("reproduction verified: rule reference matches archive")

Common Pitfalls

  • Recording a branch or a latest tag. These are mutable pointers. Only a full commit SHA guarantees the same logic on replay; capture the tag as a human-friendly label alongside it, not instead of it.
  • Trusting the SHA while ignoring a dirty tree. Uncommitted edits mean the code that ran is not the code the SHA describes. Record the clean or dirty state and refuse to certify results from a dirty tree.
  • Storing the reference only in a summary, not per result. If one batch spans a mid-run rule update, a single top-level version misattributes some rows. Stamp the reference on each evaluated feature.

Frequently Asked Questions

Why pin to a commit SHA instead of a semantic version tag?

A semantic version communicates intent to humans, but tags can be moved or deleted, and several commits may share one release tag during development. A commit SHA is content-addressed and immutable, so it uniquely identifies the exact rule logic. The recommended practice is to store both: the SHA for reproducibility and the tag for readability.

How does this fit into the broader provenance record?

Rule versioning is one facet of end-to-end lineage. The parent module on provenance and lineage tracking assembles source hashes, transform metadata, and rule references into a single reconstructable graph, and the rule reference produced here is the node that answers which logic decided the verdict.

What should happen when a rule version is rolled back?

Rolling back means future evaluations resolve to an earlier commit SHA, while historical records keep the SHA that governed them at the time. Never rewrite past audit records to reflect a rollback; the whole point of an immutable reference is that a decision reflects the rules in force when it was made.

Can I version rules held in a database rather than git?

Yes. Give each rule row an immutable version column and never update in place; instead insert a new versioned row. Your reference then becomes the rule id plus version number plus a payload digest, which offers the same reproducibility guarantees as a commit SHA.

Summary

Versioning rule references turns an audit trail from a bare pass or fail into a reproducible, defensible record. By resolving rules to an immutable commit SHA, hashing the payload actually loaded, stamping each evaluation with that reference, and verifying reproduction against a clean checkout, teams can replay and roll back compliance decisions with confidence. This capability underpins the provenance and lineage tracking module, ensuring every automated verdict names the exact logic that produced it.