How must an instance handle cascading reference updates without inconsistent intermediate states?

**Date of Record:** 2026 October 27 **Scope:** Applies when a reference's update triggers cascading updates to dependent references (schema migration, capability descriptor revision, URI retirement), and during the cascade there is a window where some references are updated and others are not, creating inconsistent intermediate states. Does not apply to atomic reference updates or cascades with no intermediate states.

Core Distinction

The separation between **individual reference update** (each reference is updated correctly) and **cascade atomicity** (the full cascade appears as a single atomic change from the perspective of external observers). Relying on individual updates creates the **Cascade Inconsistency Window**: during the cascade, Reference A is updated to v2 but Reference B (which depends on A) still points to v1, and any consumer that reads both sees an inconsistent state.

What Was Observed

In cascading reference-update architectures, failing to preserve atomicity produces three primary failure modes:

  1. **Mixed-Version Reads:** A consumer reads Reference A (already updated to v2) and Reference B (still v1). The consumer's behavior assumes consistency (A and B are compatible), but they are not, causing logic errors.
  2. **Cascade Rollback Failure:** The cascade partially fails (some references updated, some not), and rollback cannot restore a consistent state because the pre-cascade state was not preserved as a unit.
  3. **Consumer Stuck Mid-Cascade:** A consumer begins processing based on a snapshot, the cascade completes, and the consumer's subsequent reads see different state, causing unpredictable behavior.

Recorded Elements

  1. **`cascade_id_receipt`:** A unique identifier for the cascade, shared by all updates within the cascade, so consumers can group related updates.
  2. **`pre_cascade_snapshot_signature`:** A cryptographic snapshot of all references before the cascade begins, allowing rollback to a consistent state.
  3. **`cascade_commit_marker_receipt`:** A cryptographic marker indicating when the entire cascade has completed, so consumers know when they can safely re-read.
  4. **`cascade_observer_protocol_signature`:** The protocol by which consumers should behave during the cascade (e.g., "read only the pre-cascade snapshot until the commit marker arrives").

Condition of Validity

* **Applies when:** Reference updates cascade across multiple references, intermediate states are observable, and consumers can misbehave when reading mixed versions. * **Does not apply when:** Updates are strictly atomic (single operation) or cascade intermediate states are never observed.

Decision Boundary

  1. **Is the update a cascade?** If no, standard update applies. If yes, proceed.
  2. **Is a `cascade_id_receipt` and `pre_cascade_snapshot_signature` present?** If yes, the cascade can be tracked and rolled back. If no, the cascade is unprotected.
  3. **Has the `cascade_commit_marker_receipt` been received?** If yes, the cascade is complete, and consumers can read the new state. If no, consumers must follow the `cascade_observer_protocol_signature` (typically, read the pre-cascade snapshot).
  4. **Conversely, when can a consumer read during a cascade?** Only when the consumer explicitly follows the observer protocol (e.g., pins to the pre-cascade snapshot), or the specific references being read are verified to be unaffected by the cascade.

Operational Effect

  1. **Mixed-Version Prevention:** Consumers use snapshots or wait for commit, avoiding mixed reads.
  2. **Rollback Capability:** Pre-cascade snapshot enables atomic rollback if the cascade fails.
  3. **Predictable Consumer Behavior:** The observer protocol defines consistent consumer behavior during cascades.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class CascadeVerdict(Enum):
    NOT_A_CASCADE = auto()
    CASCADE_COMMITTED = auto()
    CASCADE_IN_PROGRESS = auto()
    CASCADE_UNTRACKED = auto()

@dataclass(frozen=True)
class CascadeState:
    cascade_id: str
    pre_cascade_snapshot: str | None
    commit_marker_received: bool
    references_updated: int
    references_total: int
    consumer_follows_protocol: bool

@dataclass(frozen=True)
class CascadePolicy:
    require_snapshot: bool
    require_commit_marker: bool

@dataclass(frozen=True)
class CascadeManifest:
    verdict: CascadeVerdict
    directive: str
    manifest_digest: str

class CascadingReferenceGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_cascade(
        cls, state: CascadeState, policy: CascadePolicy
    ) -> CascadeManifest:
        
        # Gate 1: Not a cascade
        if state.references_total <= 1:
            digest = cls._hash(json.dumps({
                "cascade": state.cascade_id,
                "type": "SINGLE_REFERENCE"
            }, sort_keys=True))
            return CascadeManifest(
                verdict=CascadeVerdict.NOT_A_CASCADE,
                directive=f"PROCEED: Single reference update, no cascade.",
                manifest_digest=digest
            )

        # Gate 2: Snapshot requirement
        if policy.require_snapshot and not state.pre_cascade_snapshot:
            return CascadeManifest(
                verdict=CascadeVerdict.CASCADE_UNTRACKED,
                directive=f"HALT: Cascade {state.cascade_id} lacks pre-cascade snapshot. Rollback impossible.",
                manifest_digest=cls._hash(f"NO_SNAPSHOT:{state.cascade_id}")
            )

        # Gate 3: Commit marker
        if state.commit_marker_received:
            digest = cls._hash(json.dumps({
                "cascade": state.cascade_id,
                "updated": str(state.references_updated),
                "total": str(state.references_total),
                "snapshot": state.pre_cascade_snapshot
            }, sort_keys=True))
            return CascadeManifest(
                verdict=CascadeVerdict.CASCADE_COMMITTED,
                directive=f"PROCEED: Cascade {state.cascade_id} committed ({state.references_updated}/{state.references_total}).",
                manifest_digest=digest
            )

        # Gate 4: In progress
        if policy.require_commit_marker:
            if state.consumer_follows_protocol:
                digest = cls._hash(json.dumps({
                    "cascade": state.cascade_id,
                    "status": "IN_PROGRESS",
                    "progress": f"{state.references_updated}/{state.references_total}"
                }, sort_keys=True))
                return CascadeManifest(
                    verdict=CascadeVerdict.CASCADE_IN_PROGRESS,
                    directive=f"PROCEED: Cascade in progress, consumer follows observer protocol.",
                    manifest_digest=digest
                )
            return CascadeManifest(
                verdict=CascadeVerdict.CASCADE_IN_PROGRESS,
                directive=f"HALT: Cascade in progress, consumer not following observer protocol.",
                manifest_digest=cls._hash(f"PROTOCOL_VIOLATION:{state.cascade_id}")
            )

        # Gate 5: Default
        return CascadeManifest(
            verdict=CascadeVerdict.CASCADE_IN_PROGRESS,
            directive=f"WAIT: Cascade {state.cascade_id} in progress.",
            manifest_digest=cls._hash(f"IN_PROGRESS:{state.cascade_id}")
        )