Core Architecture Regulatory Mapping

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 | Y union syntax and match-friendly enums.
  • Pydantic v2model_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.

Strict fallback precedence chain — four tiers of degrading confidence, gated by a circuit breaker, bounded by a 0.50 halt threshold resolve_compliance_data passes a circuit-breaker gate then descends Primary API (1.00), Commercial Registry (0.85), Statutory Default (0.60) and Manual Override (0.40); the first schema-valid tier calls _finalize to return a CompliancePayload with confidence and audit_hash and to write the append-only audit log. A dashed 0.50 halt line separates auto-filing tiers from the manual-review tier below it. Strict precedence chain — execution stops at the first tier whose payload passes schema validation; confidence degrades on each descent. resolve_compliance_data() entity_id · jurisdiction circuit breaker gate open ⇒ skip Tier 1, descend Tier 1 · Primary State API live portal · circuit-breaker gated conf 1.00 miss / validation fail Tier 2 · Commercial Registry licensed aggregator feed conf 0.85 miss Tier 3 · Statutory Default Matrix codified baseline · DGCL / Corp Code conf 0.60 HALT_THRESHOLD = 0.50 Tier 4 · Manual Override signed by compliance officer conf 0.40 pass ⇒ finalize _finalize() hash · log · cache-purge halt if conf < 0.50 return CompliancePayload confidence_score · audit_hash append-only audit log WORM · 7-yr retention entity_id · source · hash Tiers resolving below the 0.50 line cannot auto-file — the payload routes to the manual-override queue for human review.
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.

  1. Primary returns empty 200 OK during a maintenance window. This is RECOVERABLE. _fetch_primary returns 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 confidence 0.85. The filing proceeds automatically.
  2. Circuit breaker is open and the registry feed is also down. This is RECOVERABLE but degraded. The chain reaches the statutory default at confidence 0.60 — above the 0.50 halt line — so it files against the codified baseline (e.g. DE’s $300 minimum) and attaches the audit_hash flagging that no live source confirmed it.
  3. Resolved deadline diverges sharply from the cached value. This is AUDIT_ONLY with 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.
  4. All automated tiers fail and confidence falls below 0.50. This is CRITICAL. Do not auto-file. The payload routes to the manual-override queue at confidence 0.40; the override requires a compliance officer’s digital signature, and the system logs an UNAUTHORIZED_OVERRIDE_ATTEMPT event 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.