Compliance Fallback Chains: Deterministic Rate-Table Routing for Guild Payroll

Production accounting runs against deadlines that do not move: payroll continuity, pension and health (P&H) fund remittances, and completion-bond reporting cannot tolerate a down guild API or a missing rate table. When a primary source of rate data goes dark mid-close, the naive system either stalls — holding an entire weekly payroll hostage to one throttled endpoint — or improvises a number that no auditor can later defend. A compliance fallback chain removes that dilemma. It is a deterministic routing architecture that intercepts validation failures in primary guild data sources and transitions, tier by tier, to secondary, tertiary, or cached calculation pathways, logging every hop so the deviation is auditable rather than invisible. Engineered correctly, the chain preserves audit integrity while preventing payroll stalls, converting an unpredictable data gap into a controlled, reconcilable exception. This page specifies that chain — its state model, its production-grade Python, the collective bargaining agreements it must honor, and the quarantine and verification mechanics that make it defensible — as one subsystem of the broader Guild Compliance & Rule Validation Automation reference architecture.

Prerequisites and Expected Inputs

The implementation here targets Python 3.11+, both for zoneinfo in the standard library and for modern union-type syntax. It depends on a deliberately small stack: Pydantic v2 for boundary schema validation via model_validate and field_validator; the standard-library decimal, hashlib, and zoneinfo modules for currency-safe arithmetic, deterministic audit hashing, and timezone-aware timestamps; and, for production deployments, a signature library such as PyJWT or cryptography to verify cached snapshots before they enter the payroll ledger. Never use float for monetary values — a fractional-cent drift, compounded across thousands of contribution lines, becomes exactly the variance a guarantor will ask you to explain.

The chain assumes its inputs are already clean. Records do not originate here: heterogeneous timecards and cost reports are normalized upstream by the Cost Ingestion & Data Parsing Workflows subsystem, where Async Batch Processing absorbs vendor API rate limits and Schema Validation & Error Handling quarantines malformed payloads before they reach any rule engine. Each incoming hour is keyed against the taxonomy defined in Cost Code Standardization, so a validated hour maps to exactly one fund code, one department, and one budget line. What the fallback chain adds is a single guarantee on top of that contract: the rate applied to a validated hour is always resolvable to a signed, version-stamped source, even when the preferred source is unavailable.

The primary input the chain consumes is a rate-table lookup keyed by guild, jurisdiction, contract tier, and the date the work was performed. The output is a resolved rate plus a provenance record — the tier that produced it, the SHA-256 hash of the payload it came from, and the timestamp of the routing decision.

Architecture: A Fallback Chain Is a State Machine, Not a Retry Loop

The most common design error is to treat fallback as a try/except wrapped in a retry counter. That produces non-determinism: the same input can resolve to different rates depending on transient timing, and the reason a given number was chosen is lost. A production-ready chain is instead a strict state machine in which each tier activates only on validation failure of the prior one, and every transition is committed to an append-only ledger before the next tier is attempted.

Primary ingestion attempts to pull live rate tables, union jurisdiction flags, and daily call-sheet metadata from the guild API. If that endpoint returns a 4xx or 5xx status, a malformed JSON payload, or an out-of-date version stamp, the pipeline advances to Tier 1: a locally cached, cryptographically signed snapshot of the most recent ratified agreement. Should Tier 1 fail validation against the current production schedule or budget code, Tier 2 activates, applying adjacent-tier rate interpolation or historical-precedent mapping. A terminal conservative default guarantees the chain always resolves rather than raising into the payroll run. Every transition carries an immutable timestamp, an operator ID, and a hash-verified payload so a completion-bond auditor can reconstruct exactly why a rate was chosen.

The cascade below shows each tier activating only when the prior tier fails validation, logging the transition before advancing.

Tiered fallback cascade as a state machine Four rate-source tiers are stacked in order: Tier 0 Primary API, Tier 1 signed cached snapshot, Tier 2 adjacent-tier interpolation, and Tier 3 conservative floor default. Each tier advances to the one below it only on validation failure — a 4xx or 5xx, a malformed or stale payload, a failed schedule check, or no adjacent baseline. Whichever tier validates writes its result into a single append-only audit ledger that records the tier, a SHA-256 payload hash, and a UTC timestamp. The terminal conservative default always resolves, so the chain never raises into the payroll run. 4xx / 5xx, malformed, or stale fails schedule validation no adjacent-tier baseline valid valid valid resolves Tier 0 — Primary API live guild rate feed Tier 1 — Cached Snapshot signed · schedule-checked Tier 2 — Interpolation adjacent-tier baseline Tier 3 — Conservative Default floor rate · always resolves Append-only Audit Ledger logged per resolution: tier · SHA-256 hash · UTC timestamp valid → ledger fails validation → next tier
Each tier activates only when the prior one fails validation; whichever tier validates writes one hashed entry to the append-only ledger, and the terminal default always resolves.

Two properties make this model defensible rather than merely functional. The first is that failure is classified, not swallowed: a stale version stamp routes differently from a network 503, and both are distinguishable in the log from a signature mismatch. The second is that every tier is idempotent — re-running the chain against the same inputs produces the same resolved rate and the same ledger hash, which is what lets a production accountant replay a disputed week without duplicating a single contribution.

Core Implementation

The reference implementation models the rate table as a Pydantic v2 object validated at the boundary, resolves through the tiers as an explicit state machine, and writes a hashed provenance entry at every transition. Monetary fields are Decimal; audit timestamps are timezone-aware.

import hashlib
import json
import logging
from datetime import datetime
from decimal import Decimal
from enum import Enum
from zoneinfo import ZoneInfo
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, field_validator

logging.basicConfig(
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    level=logging.INFO,
)
logger = logging.getLogger("compliance_fallback_chain")

# Audit timestamps are stamped in UTC and rendered against the production
# hub's IANA zone only for human-facing reports — never with a fixed offset.
AUDIT_TZ = ZoneInfo("America/Los_Angeles")


class FallbackTier(str, Enum):
    PRIMARY_API = "primary_api"
    CACHED_SNAPSHOT = "cached_snapshot"
    INTERPOLATION = "interpolation"
    CONSERVATIVE_DEFAULT = "conservative_default"


class GuildRateTable(BaseModel):
    """Boundary schema for any rate table entering the chain, from any tier."""
    model_config = ConfigDict(frozen=True)

    guild: str
    jurisdiction: str
    contract_tier: str
    base_rate: Decimal = Field(gt=Decimal("0"))
    version: str
    effective_from: str  # ISO date the agreement took force

    @field_validator("base_rate", mode="before")
    @classmethod
    def coerce_decimal(cls, v: Any) -> Decimal:
        # Reject float inputs outright; parse strings/ints exactly.
        if isinstance(v, float):
            raise ValueError("base_rate must not be a float — pass str/Decimal")
        return Decimal(str(v))

    @field_validator("jurisdiction")
    @classmethod
    def known_jurisdiction(cls, v: str) -> str:
        if v not in {"Zone_A", "Zone_B", "Zone_Default"}:
            raise ValueError(f"unmapped jurisdiction: {v}")
        return v


class LedgerEntry(BaseModel):
    tier: FallbackTier
    payload_hash: str
    resolved_rate: Decimal
    timestamp: str
    operator_id: str


class ComplianceFallbackChain:
    def __init__(self, production_id: str, budget_code: str):
        self.production_id = production_id
        self.budget_code = budget_code
        self.audit_trail: list[LedgerEntry] = []

    def _hash(self, payload: bytes) -> str:
        return hashlib.sha256(payload).hexdigest()

    def _canonical_bytes(self, table: GuildRateTable) -> bytes:
        # Deterministic serialization so identical tables hash identically.
        return json.dumps(
            table.model_dump(mode="json"), sort_keys=True, separators=(",", ":")
        ).encode("utf-8")

    def resolve(self, api_response: bytes | None) -> LedgerEntry:
        # Tier 0 — Primary API
        table = self._try_primary(api_response)
        if table is not None:
            return self._log(FallbackTier.PRIMARY_API, table)

        logger.warning("Primary rate source failed; entering fallback routing.")

        # Tier 1 — Cryptographically signed local snapshot
        table = self._load_signed_cache()
        if table is not None and self._validates_against_schedule(table):
            return self._log(FallbackTier.CACHED_SNAPSHOT, table)

        # Tier 2 — Adjacent-tier interpolation (over-accrues, never under)
        table = self._interpolate()
        if table is not None:
            return self._log(FallbackTier.INTERPOLATION, table)

        # Tier 3 — Terminal conservative default; the chain always resolves
        return self._log(FallbackTier.CONSERVATIVE_DEFAULT, self._conservative_default())

    def _log(self, tier: FallbackTier, table: GuildRateTable) -> LedgerEntry:
        payload = self._canonical_bytes(table)
        entry = LedgerEntry(
            tier=tier,
            payload_hash=self._hash(payload),
            resolved_rate=table.base_rate,
            timestamp=datetime.now(tz=ZoneInfo("UTC")).isoformat(),
            operator_id="SYS_AUTO",
        )
        self.audit_trail.append(entry)
        logger.info("tier=%s hash=%s rate=%s", tier.value, entry.payload_hash, entry.resolved_rate)
        return entry

    def _try_primary(self, api_response: bytes | None) -> GuildRateTable | None:
        if api_response is None or b"error" in api_response:
            return None
        try:
            return GuildRateTable.model_validate(json.loads(api_response))
        except Exception as exc:  # malformed JSON, schema violation, bad rate
            logger.warning("Primary payload rejected: %s", exc)
            return None

    def _load_signed_cache(self) -> GuildRateTable | None:
        # In production, verify a detached signature (PyJWT / cryptography)
        # against a pinned public key BEFORE trusting the snapshot.
        snapshot = {
            "guild": "SAG-AFTRA", "jurisdiction": "Zone_A", "contract_tier": "high_budget_svod",
            "base_rate": "450.00", "version": "2026.1", "effective_from": "2026-01-01",
        }
        try:
            return GuildRateTable.model_validate(snapshot)
        except Exception:
            return None

    def _validates_against_schedule(self, table: GuildRateTable) -> bool:
        return table.jurisdiction in {"Zone_A", "Zone_B"}

    def _interpolate(self) -> GuildRateTable | None:
        # Adjacent-tier baseline: deliberately conservative.
        return GuildRateTable.model_validate({
            "guild": "SAG-AFTRA", "jurisdiction": "Zone_Default", "contract_tier": "interpolated",
            "base_rate": "425.00", "version": "INTERPOLATED", "effective_from": "2026-01-01",
        })

    def _conservative_default(self) -> GuildRateTable:
        return GuildRateTable.model_validate({
            "guild": "SAG-AFTRA", "jurisdiction": "Zone_Default", "contract_tier": "floor",
            "base_rate": "400.00", "version": "FLOOR", "effective_from": "2026-01-01",
        })

The frozen=True config makes every resolved table immutable once validated, so a downstream caller cannot mutate a rate after it has been hashed into the ledger. Because serialization is canonical — sorted keys, no incidental whitespace — the SHA-256 hash is a stable fingerprint: two runs that resolve the same table produce the same hash, and any tampering with a cached snapshot changes it. That fingerprint is the spine of the whole audit story.

Guild and Contract Specifics

Fallback routing is not a single formula; it is per-guild conditional logic tailored to each collective bargaining agreement’s (CBA) rate structure, and the “conservative” default means something different for each.

Screen Actors Guild – American Federation of Television and Radio Artists (SAG-AFTRA). Residual and session-rate calculations depend on exhibition windows, market classification, and streaming budget tier — high-budget subscription video on demand (SVOD) versus low-budget, versus ad-supported distribution. When the primary feed for streaming metrics or theatrical-gross thresholds is delayed, the chain must default to the rate table that over-accrues, never under-accrues, so the P&H funds are never shorted. The conservative path here mirrors the stateful approach detailed in SAG-AFTRA Residuals Logic, where a provisional accrual is later reconciled against verified exhibition data rather than treated as final.

Directors Guild of America (DGA). Overtime premiums and turnaround penalties under the DGA Basic Agreement depend on call and wrap times, which means the fallback rate is only half the problem — the hours it multiplies are timezone-sensitive. If real-time timecard validation fails, the chain applies the most conservative contractual rate consistent with the agreement in force on the work date, deferring the penalty computation to the verified-hours pass rather than guessing. The penalty structure the fallback must respect is specified in DGA Overtime & Turnaround Rules; the fallback’s job is only to guarantee a defensible base rate while those hours are pending.

Pension, Health & Retirement funds. The Motion Picture Industry P&H plans require exact hour tracking against jurisdictional wage floors, and a fringe multiplier applied to the wrong base is a fund-remittance error, not a rounding quibble. The chain isolates each P&H calculation into an idempotent micro-process that can re-run without duplicating a contribution — the mechanics of which are developed in Pension & Health Fund Calculations. This is why the terminal tier resolves to a floor rate rather than a zero or a null: a floor over-accrues predictably, and over-accrual is reconcilable, whereas a missing contribution surfaces as a delinquency.

The rate table itself is versioned by ratified agreement: a CBA is a succession of amendments, each governing a date range, so effective_from is a first-class field. A cached snapshot is only valid as a fallback if its effective date brackets the work date — a Tier 1 snapshot from last season’s agreement must fail validation and route onward, not silently reprice a settled week. The most conservative rule that survives that date check is the one the chain applies.

Error Handling and Quarantine

Not every failure should route forward. Some inputs are not a case of “the preferred source is down” but “this record is wrong,” and those must be quarantined rather than fed into interpolation. The rule is: routing handles source availability; quarantine handles record validity. A 5xx from the guild API routes to the cached snapshot. A rate table whose base_rate is negative, whose jurisdiction is unmapped, or whose signature fails verification is not a candidate for fallback at all — it is quarantined.

Every quarantine event serializes the original payload verbatim, attaches the SHA-256 hash of that payload, records a machine-readable reason code, and pushes the record to a reconciliation queue for human triage. Crucially, the failing payload never enters the ledger as a resolved rate — it enters a separate exception store, so the payroll ledger stays clean while the discrepancy is preserved for review. When unionized and non-union crew share a set, jurisdictional conflicts can trigger cascading validation failures; those edge cases route to an explicit arbitration threshold rather than an ambiguous midpoint, so every derived rate is legally defensible and manually reviewable before it is committed.

The quarantine contract mirrors the boundary discipline of Schema Validation & Error Handling: a rejected record carries a reason code, its original bytes, and its hash, so a production accountant can triage without re-ingesting the whole batch. The specifics of assembling and validating replacement rate tables when a primary source is missing entirely are covered in Building Fallback Chains for Missing Guild Rate Tables.

Routing versus quarantine: two failure classes, two destinations A validated hour and its rate-table lookup enter a classifier that separates failures by type. Source-availability failures — 4xx, 5xx, timeout, or a stale version — take the upper path into the fallback routing cascade, which advances tier by tier and always resolves to the append-only audit ledger. Record-validity failures — a negative rate, an unmapped jurisdiction, or a signature mismatch — take the lower path into quarantine, where the original payload is stored with its SHA-256 hash and a machine-readable reason code and pushed to an exception queue for human triage. A quarantined record never enters the audit ledger; the payroll ledger stays clean while the discrepancy is preserved. source unavailable 4xx · 5xx · timeout · stale record invalid Validated hour rate-table lookup Classify failure Fallback routing cascades tier → tier Audit Ledger always resolves · hashed Quarantine record payload + SHA-256 + reason code Exception Queue human triage · reconcile negative rate · unmapped jurisdiction · signature mismatch never enters the ledger
Routing handles source availability and always resolves to the ledger; quarantine handles record validity and diverts to the exception queue, so an invalid payload is preserved for triage but never reaches the payroll ledger.

Verification

A fallback chain is only trustworthy if you can prove, after the fact, which tier produced every rate. Verification therefore checks three artifacts, not one.

First, the ledger entry. Every resolved rate must produce exactly one LedgerEntry whose tier names the source, whose payload_hash matches an independent re-hash of the canonical payload, and whose resolved_rate is a Decimal. Re-running resolve() against the same inputs must yield an identical hash — the idempotency check that lets you replay a disputed week deterministically.

Second, the audit log fields. Each transition must log, at minimum: production_id, budget_code, tier, payload_hash, resolved_rate, a UTC timestamp, and an operator_id (SYS_AUTO for automated routing, a real ID for manual overrides). A completion-bond auditor reads this sequence as the provenance of the number, so no field is optional.

Third, the reconciliation report shape. A weekly report should surface, per fund code: how many rates resolved at the primary tier versus each fallback tier, the aggregate over-accrual introduced by conservative defaults, and every quarantined record awaiting triage. A healthy run is mostly primary-tier with a short, explained fallback tail; a run that is largely interpolated is a signal that a primary source needs attention before it becomes a payroll problem. Surfacing that drift ahead of the weekly run — rather than discovering it in a cost-report review — is the entire point of instrumenting the chain.

A minimal verification harness confirms the invariants:

def verify(entry: LedgerEntry, chain: ComplianceFallbackChain, raw: bytes | None) -> None:
    replay = chain.resolve(raw)
    assert replay.payload_hash == entry.payload_hash, "non-deterministic resolution"
    assert isinstance(entry.resolved_rate, Decimal), "rate must be Decimal"
    assert entry.tier in FallbackTier, "unknown tier in ledger"
    assert entry.operator_id, "every entry needs an accountable operator id"

When engineered with signature verification, idempotent processing, and conservative accrual defaults, a compliance fallback chain turns an unpredictable data gap into a controlled, auditable workflow: payroll does not stall, the completion guarantor’s scrutiny is satisfied, and every rate remains traceable to a signed, version-stamped source across a high-velocity shooting schedule.

Up: Guild Compliance & Rule Validation Automation