Implementing Jurisdictional Fallback Rules for Compliance Data
This guide sits inside the Compliance Metadata Schemas layer of the Core Architecture & Regulatory Mapping framework: once a payload is typed and validated, this page covers what the engine does when the source of that payload — a Secretary of State portal or commercial registry — fails to answer reliably.
Scope
This page implements a single, end-to-end fallback resolver: given an entity_id and a jurisdiction, return the most authoritative compliance values available (filing deadline, statutory fee, registered-agent requirement) along with a confidence score and an immutable audit hash. It covers the precedence chain, circuit breaking, cache invalidation, and the escalation boundary to a human compliance officer.
It deliberately excludes the upstream typing contract (owned by the parent compliance metadata schemas guide), the network-layer retry and rate-limit mechanics of talking to a portal (owned by Secretary of State Portal API Ingestion), and deadline arithmetic itself (owned by State Filing Deadline Calendars). This resolver consumes those layers; it does not reimplement them.
Statutory Constraint Driving Fallback
Fallback logic must be anchored in statutory authority, not heuristic interpolation. A missing portal response cannot license the engine to guess a deadline, because the underlying obligation still has a fixed legal basis: Delaware franchise tax and annual report duties arise under DGCL § 503 with a March 1 due date and a statutory minimum tax; California’s Statement of Information is fixed to the formation-anniversary window under Cal. Corp. Code § 1502; New York runs a biennial Statement cadence under N.Y. BSC § 408. When a source goes dark, the correct behavior is to fall back to the codified value for that statute — a known-good baseline — and to record that the value came from a default rather than from the live registry, so an auditor can later reconstruct exactly which authority the filing relied on.
Prerequisites
- Python 3.10+ — uses
X | Yunion syntax andmatch-friendly enums. - Pydantic v2 —
model_dump(mode="json")for deterministic, JSON-safe hashing. asyncio(stdlib) — non-blocking traversal so the resolver never stalls the scheduler.- A cache client with a
.delete(key)method (Redis or an in-memory shim) for invalidation on fallback success. - Read access to the primary portal API or authenticated scrape session, plus a licensed commercial-registry feed for the secondary tier.
- A write-once audit sink (append-only S3, WORM bucket, or ledger DB) for the generated audit records.
Implementation
The resolver traverses a strict precedence chain. Each tier is a node in a directed acyclic graph; execution stops at the first tier that returns a payload passing schema validation, and the confidence score degrades predictably as the engine descends.
from __future__ import annotations
import asyncio
import hashlib
import json
import logging
import time
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel, Field
# Structured JSON logging so every fallback decision is machine-parseable for audit.
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("compliance.fallback_engine")
class Jurisdiction(str, Enum):
DE = "DE"
CA = "CA"
NY = "NY"
TX = "TX"
class FallbackSource(str, Enum):
PRIMARY_API = "primary_api"
COMMERCIAL_REGISTRY = "commercial_registry"
STATUTORY_DEFAULT = "statutory_default"
MANUAL_OVERRIDE = "manual_override"
class CompliancePayload(BaseModel):
entity_id: str
jurisdiction: Jurisdiction
filing_deadline: Optional[datetime] = None
statutory_fee: Optional[float] = None
registered_agent_required: Optional[bool] = None
source: FallbackSource = FallbackSource.PRIMARY_API
confidence_score: float = Field(ge=0.0, le=1.0, default=1.0)
audit_hash: str = ""
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
def generate_audit_hash(self) -> str:
# mode="json" serializes datetime -> ISO 8601 and Enum -> .value, so json.dumps
# never raises on a raw datetime/Enum. This digest is the non-repudiation anchor.
serializable = self.model_dump(mode="json", exclude={"audit_hash"})
payload_bytes = json.dumps(serializable, sort_keys=True).encode()
return hashlib.sha256(payload_bytes).hexdigest()
class CircuitBreaker:
"""Trips open after repeated primary-source failures to stop hammering a dead portal."""
def __init__(self, failure_threshold: int = 3, cooldown_seconds: float = 60.0):
self.failure_threshold = failure_threshold
self.cooldown_seconds = cooldown_seconds
self.failures = 0
self.last_failure_time = 0.0
self.state = "closed"
def record_success(self) -> None:
self.failures = 0
self.state = "closed"
def record_failure(self) -> None:
self.failures += 1
self.last_failure_time = time.time()
if self.failures >= self.failure_threshold:
self.state = "open"
def allow_request(self) -> bool:
if self.state == "closed":
return True
# After cooldown, allow a single probe (half-open) before fully reopening.
if self.state == "open" and (time.time() - self.last_failure_time) > self.cooldown_seconds:
self.state = "half-open"
return True
return False
# Codified baseline per statute — the known-good value used when live sources fail.
STATUTORY_DEFAULTS: dict[Jurisdiction, dict[str, Any]] = {
Jurisdiction.DE: {"deadline": "2026-03-01", "fee": 300.0, "ra_required": True}, # DGCL § 503
Jurisdiction.CA: {"deadline": "2026-04-01", "fee": 25.0, "ra_required": False}, # Corp. Code § 1502
Jurisdiction.NY: {"deadline": "2026-02-01", "fee": 9.0, "ra_required": True}, # BSC § 408 (biennial)
Jurisdiction.TX: {"deadline": "2026-05-15", "fee": 0.0, "ra_required": True}, # Tax Code § 171 margin report
}
# Confidence degrades by tier; anything below HALT_THRESHOLD must not auto-file.
CONFIDENCE = {
FallbackSource.PRIMARY_API: 1.0,
FallbackSource.COMMERCIAL_REGISTRY: 0.85,
FallbackSource.STATUTORY_DEFAULT: 0.60,
FallbackSource.MANUAL_OVERRIDE: 0.40,
}
HALT_THRESHOLD = 0.50
class FallbackEngine:
def __init__(self, cache_client: Optional[Any] = None):
self.cache = cache_client
self.breakers: dict[Jurisdiction, CircuitBreaker] = {j: CircuitBreaker() for j in Jurisdiction}
async def _fetch_primary(self, entity_id: str, jurisdiction: Jurisdiction) -> Optional[dict[str, Any]]:
try:
# Hard timeout: a slow portal must not block the scheduler thread.
return await asyncio.wait_for(self._call_portal(jurisdiction), timeout=3.0)
except asyncio.TimeoutError:
logger.warning(json.dumps({"event": "primary_timeout", "entity_id": entity_id,
"jurisdiction": jurisdiction.value}))
return None
except Exception as exc: # network/parse failure -> treat as a tier miss, not a crash
logger.error(json.dumps({"event": "primary_failure", "entity_id": entity_id,
"jurisdiction": jurisdiction.value, "error": str(exc)}))
return None
async def _fetch_secondary(self, entity_id: str, jurisdiction: Jurisdiction) -> Optional[dict[str, Any]]:
# Licensed commercial-registry aggregator feed; slightly less authoritative than the state itself.
return {"deadline": "2026-04-16", "fee": 310.0, "ra_required": True}
async def resolve_compliance_data(self, entity_id: str, jurisdiction: Jurisdiction) -> CompliancePayload:
breaker = self.breakers[jurisdiction]
# Tier 1 — Primary state API, gated by the circuit breaker.
if breaker.allow_request():
primary = await self._fetch_primary(entity_id, jurisdiction)
if primary:
breaker.record_success()
return self._finalize(entity_id, jurisdiction, primary, FallbackSource.PRIMARY_API)
breaker.record_failure()
# Tier 2 — Commercial registry.
secondary = await self._fetch_secondary(entity_id, jurisdiction)
if secondary:
return self._finalize(entity_id, jurisdiction, secondary, FallbackSource.COMMERCIAL_REGISTRY)
# Tier 3 — Statutory default baseline (always available).
return self._finalize(entity_id, jurisdiction, STATUTORY_DEFAULTS[jurisdiction],
FallbackSource.STATUTORY_DEFAULT)
def _finalize(self, entity_id: str, jurisdiction: Jurisdiction, raw: dict[str, Any],
source: FallbackSource) -> CompliancePayload:
payload = CompliancePayload(
entity_id=entity_id,
jurisdiction=jurisdiction,
filing_deadline=datetime.strptime(raw["deadline"], "%Y-%m-%d").replace(tzinfo=timezone.utc),
statutory_fee=raw.get("fee"),
registered_agent_required=raw.get("ra_required"),
source=source,
confidence_score=CONFIDENCE[source],
)
if source is not FallbackSource.PRIMARY_API:
# A fallback fired: purge stale cache so downstream reads see the corrected value.
self._invalidate_cache(entity_id, jurisdiction)
payload.audit_hash = payload.generate_audit_hash()
logger.info(json.dumps({
"event": "fallback_resolved", "entity_id": entity_id, "jurisdiction": jurisdiction.value,
"source": source.value, "confidence": payload.confidence_score, "audit_hash": payload.audit_hash,
"halt_required": payload.confidence_score < HALT_THRESHOLD,
}))
return payload
def _invalidate_cache(self, entity_id: str, jurisdiction: Jurisdiction) -> None:
if not self.cache:
return
for key in (f"compliance:{entity_id}:{jurisdiction.value}:deadline",
f"compliance:{entity_id}:{jurisdiction.value}:fee"):
self.cache.delete(key)
async def _call_portal(self, jurisdiction: Jurisdiction) -> dict[str, Any]:
await asyncio.sleep(0.1) # stand-in for the real authenticated request
return {"deadline": "2026-04-15", "fee": 300.0, "ra_required": True}
The compliance-critical line is generate_audit_hash, which calls self.model_dump(mode="json", ...). Pydantic v2’s mode="json" serializes datetime to ISO 8601 strings and Enum members to their .value, producing a dict that json.dumps can hash without a custom encoder. A plain model_dump() returns live datetime/Enum objects and raises TypeError — which would silently break the non-repudiation guarantee.
Configuration Reference
| Parameter | Default | Legal / operational justification |
|---|---|---|
| Primary fetch timeout | 3.0s |
Portals routinely stall under peak filing load; a hard ceiling stops a slow source from blocking the scheduler. |
Circuit-breaker failure_threshold |
3 |
Three consecutive misses distinguishes a transient blip from a degraded endpoint before the breaker trips open. |
Circuit-breaker cooldown_seconds |
60.0 |
Gives an overloaded Secretary of State portal room to recover; prevents retry storms that trigger rate-limit bans. |
HALT_THRESHOLD |
0.50 |
Below this confidence the value is too weak to file on; the payload routes to manual review instead of auto-submission. |
| Confidence: statutory default | 0.60 |
A codified baseline is correct for the statute but may miss entity-specific facts, so it sits just above the halt line. |
STATUTORY_DEFAULTS deadlines |
per statute | DE § 503 (Mar 1), CA § 1502 (anniversary), NY § 408 (biennial), TX § 171 (May 15) — known-good legal floors. |
| Audit retention | 7 years | Typical statutory record-retention minimum; the SHA-256 audit_hash must remain reconstructable for that window. |
Portal behavior dictates which trigger fires before the chain even begins. Encode these per jurisdiction rather than as a single generic timeout:
| Jurisdiction | Observed portal behavior | Fallback trigger |
|---|---|---|
| Delaware (DE) | Token rate limits exceeded; empty 200 OK during maintenance windows |
Immediate fall-through to the commercial registry tier |
| California (CA) | DOM/selector shift on BizFile causing scrape parse failure | Schema-validation failure → statutory default |
| New York (NY) | Session timeout after ~90s idle; CAPTCHA injection on HTML portal | Circuit breaker trip → manual-override queue |
| Texas (TX) | Combined-group margin report returns ambiguous duplicate rows | Reject at validation; resolve against combined-group parent |
Failure Modes and Fallback Routing
These map onto the parent cluster’s error-categorization taxonomy — CRITICAL (abort), RECOVERABLE (repair via fallback), AUDIT_ONLY (proceed with a warning) — defined in the compliance metadata schemas guide.
- Primary returns empty
200 OKduring a maintenance window. This isRECOVERABLE._fetch_primaryreturns the payload, but a downstream validation against the schema rejects the empty body; the breaker records a failure and the chain descends to the commercial registry at confidence0.85. The filing proceeds automatically. - Circuit breaker is open and the registry feed is also down. This is
RECOVERABLEbut degraded. The chain reaches the statutory default at confidence0.60— above the0.50halt line — so it files against the codified baseline (e.g. DE’s $300 minimum) and attaches theaudit_hashflagging that no live source confirmed it. - Resolved deadline diverges sharply from the cached value. This is
AUDIT_ONLYwith teeth. The cache is invalidated on any non-primary resolution; if the new deadline differs from the prior cached value by more than the tolerance you set, attach a deviation warning to the submission manifest and reconcile against the State Filing Deadline Calendars matrix before submitting. - All automated tiers fail and confidence falls below
0.50. This isCRITICAL. Do not auto-file. The payload routes to the manual-override queue at confidence0.40; the override requires a compliance officer’s digital signature, and the system logs anUNAUTHORIZED_OVERRIDE_ATTEMPTevent for any unsigned submission.
Frequently Asked Questions
Why not just retry the primary portal instead of falling through to a default?
Retries are the right tool for a transient blip, and the network layer in Secretary of State Portal API Ingestion handles those with backoff. The fallback chain is for sustained degradation: once the circuit breaker is open, more retries only deepen a rate-limit ban. Falling through to a codified statutory baseline keeps the filing deadline calculable while the portal recovers, and the confidence score records that the value came from a default rather than the live registry.
Is filing against a statutory default ever safe?
For fixed-basis obligations, yes. Delaware’s $300 LLC franchise tax and March 1 due date under DGCL § 503 do not change per entity, so the default is the same value the portal would have returned. It is unsafe for entity-specific facts — an authorized-shares franchise-tax calculation, or a California anniversary that depends on formation date — which is why those jurisdictions degrade through the commercial-registry tier first and why anything resolved below 0.50 confidence halts for human review.
Why hash the payload with mode="json" specifically?
The audit hash must be deterministic and reproducible years later. model_dump(mode="json") normalizes datetime to ISO 8601 and Enum to its .value, so the same logical payload always serializes to the same bytes regardless of the Python object identity at runtime. A plain model_dump() would leave live datetime/Enum objects that json.dumps cannot serialize, breaking the non-repudiation guarantee the audit trail depends on.
Where should the immutable audit records be stored?
In write-once storage — an append-only S3 bucket with object lock, WORM storage, or a ledger-backed compliance DB — never an overwritable table. Index each record by entity_id, jurisdiction, and audit_hash, and retain for the statutory minimum (commonly 7 years). Manual overrides additionally require a bound digital signature; the resolver rejects unsigned overrides outright.