Resolving Amendment Conflicts with Latest-Effective-Wins

When three amendments each rewrite the same base_rent, the abstraction system has to answer one question with zero ambiguity: which value is in force on a given date, and why? This page specifies the resolver that sits on top of the Amendment Versioning & Ledgers model — not how amendment events are stored (that append-only design is covered in modeling append-only lease ledgers for audit trails), but how a stored stack of events collapses into a single winning value per field. The rule is latest effective amendment wins, with a deterministic tie-break chain so the same inputs always produce the same output on every worker, in every run.

The naive version — “sort by the amendment’s own date and take the last one” — breaks the first time a landlord executes two riders on the same day, or backdates an amendment so its effective date precedes one signed months earlier. A correct resolver separates when a change takes effect from when it was signed from the order it arrived, resolves each field independently rather than swapping whole documents, and emits provenance for the value it picks so auditors and downstream systems can trace it.

Architectural context

The resolver is a pure function that consumes the validated event ledger and produces the as-of snapshot other systems read. Upstream, each amendment has already been parsed and coerced against the metadata normalization standards — dates are ISO-8601, money is Decimal, and field names match the canonical lease data models. Downstream, the resolved snapshot feeds rent schedules, escalation formula mapping, and critical-date automation, so a wrong winner silently corrupts every derived number. The resolver never parses documents and never mutates the ledger; it reads an ordered list of events for one field and returns the value in force plus its source. When it cannot pick a winner deterministically, it does not guess — it routes the field through fallback routing logic to manual review.

Latest-effective-wins resolution for a single lease field Four events for the field base_rent enter a resolver: a base value of 12,000 dollars, amendment A1 effective 2024-01-01 executed 2023-11-15 setting 12,500 dollars, amendment A2 effective 2025-01-01 executed 2024-12-01 setting 13,000 dollars, and amendment A3 effective 2025-01-01 executed 2024-12-20 setting 13,250 dollars. The resolver sorts by a three-part key: effective date first, then execution date, then a stable ingestion sequence. Because A2 and A3 share the same effective date of 2025-01-01, the tie is broken by execution date, and A3's later execution date wins. The winning value is 13,250 dollars with provenance recorded as amendment A3, effective 2025-01-01. Events · field: base_rent base canonical value · $12,000.00 A1 · set $12,500.00 eff 2024-01-01 · exec 2023-11-15 seq 1 A2 · set $13,000.00 eff 2025-01-01 · exec 2024-12-01 seq 2 A3 · set $13,250.00 eff 2025-01-01 · exec 2024-12-20 seq 3 Deterministic sort key 1 · effective_date 2 · execution_date (tie-break) 3 · sequence (stable) last key wins · field-level fold Tie: A2 & A3 share eff 2025-01-01 execution date breaks it → A3 (2024-12-20) > A2 (2024-12-01) Resolved value in force $13,250.00 provenance · source = A3 effective 2025-01-01 · field base_rent
Latest-effective-wins on one field: order by effective date, break ties on execution date, then a stable sequence; fold to the last key and record which amendment supplied the winning value.

The tie-break chain, in priority order

Resolution is a total order over a field’s events. Each level exists to disambiguate a tie in the level above it; the sequence number guarantees the order is total even when the first two components collide exactly.

Priority Key component What it answers Why it ranks here
1 effective_date When does the change take force? The lease’s own economics key off effective date, not signature date
2 execution_date Which was signed later for the same effective day? Two riders effective the same day: the later signature reflects the parties’ final intent
3 sequence Stable ingestion order when 1 & 2 both tie Guarantees a deterministic winner even for same-day, same-execution events

Compare that to a document-level swap, which is what most first-generation abstraction tools do — and why they drift:

Aspect Document-level resolution Field-level resolution (recommended)
Unit of override Whole amendment replaces prior state Each field resolved from its own event stream
Untouched fields Risk being blanked by a partial amendment Retain the last value that actually set them
Provenance “Amendment A3 is current” Per field: base_rent from A3, cam_cap from A1
Compound fields All-or-nothing Sub-keys resolved independently
Audit answer Coarse Traces every field to its winning event

Field-level resolution is the non-negotiable choice for leases because a single amendment rarely touches every term. A rent rider that only changes base_rent must not wipe the renewal_option an earlier amendment established. Resolve each field against its own ordered event list; never let one document’s silence overwrite another’s assertion.

The resolver takes a base value and the events that touch one field, sorts them by the three-part key, applies any explicit supersessions, then folds left so the maximum key wins. It returns both the value and its provenance. Money is Decimal throughout; a float here would eventually post a rounding error into a rent schedule.

from __future__ import annotations

from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from enum import Enum
from typing import Optional

from pydantic import BaseModel, Field, model_validator


class OverrideMode(str, Enum):
    SET = "set"                 # explicit new value for the field
    REVERT_TO_BASE = "revert"   # null semantics: restore the canonical base value


class AmendmentEvent(BaseModel):
    """One field-level change carried by a single amendment."""
    amendment_id: str
    field: str
    effective_date: date
    execution_date: date
    sequence: int = Field(ge=0)               # stable ingestion order, monotonic
    mode: OverrideMode = OverrideMode.SET
    value: Optional[Decimal] = None           # required unless reverting
    supersedes: Optional[str] = None          # explicit supersession of a prior amendment_id

    @model_validator(mode="after")
    def _require_value_when_setting(self) -> "AmendmentEvent":
        if self.mode is OverrideMode.SET and self.value is None:
            raise ValueError(f"{self.amendment_id}: mode 'set' requires a value")
        return self


@dataclass(frozen=True)
class ResolvedField:
    field: str
    value: Optional[Decimal]
    source: str                 # winning amendment_id, or "base"
    effective_date: Optional[date]
    reverted_to_base: bool
    superseded: tuple[str, ...]  # amendment_ids explicitly dropped


def _sort_key(event: AmendmentEvent) -> tuple[date, date, int]:
    # Total, deterministic order: effective, then execution, then stable sequence.
    return (event.effective_date, event.execution_date, event.sequence)


def resolve_field(
    field: str,
    base_value: Optional[Decimal],
    events: list[AmendmentEvent],
) -> ResolvedField:
    relevant = sorted((e for e in events if e.field == field), key=_sort_key)

    # Explicit supersession removes a named amendment regardless of its key.
    superseded = {e.supersedes for e in relevant if e.supersedes}
    ordered = [e for e in relevant if e.amendment_id not in superseded]

    value, source, effective, reverted = base_value, "base", None, False
    for event in ordered:  # ascending order -> last (max key) wins
        effective = event.effective_date
        if event.mode is OverrideMode.REVERT_TO_BASE:
            value, source, reverted = base_value, event.amendment_id, True
        else:
            value, source, reverted = event.value, event.amendment_id, False

    return ResolvedField(
        field=field,
        value=value,
        source=source,
        effective_date=effective,
        reverted_to_base=reverted,
        superseded=tuple(sorted(superseded)),
    )

Two design choices carry the correctness. First, the sort key is a plain tuple, so Python’s stable sort gives a total order — there is no path where two events compare equal and the winner depends on input ordering. Second, the fold is left-to-right over the ascending sort, so the event with the greatest key is applied last and wins; provenance is simply whatever event the loop applied last. Explicit supersession is handled before the fold: a superseding amendment names the amendment_id it voids, and that event is dropped from the stream entirely rather than merely out-ranked. That is the difference between implicit override — a later effective date silently winning — and explicit supersession — a document that says “Amendment 2 is hereby cancelled.”

A resolver for a compound field (say a renewal_option with notice_days and option_rent sub-keys) calls resolve_field once per sub-key with its own event stream, so a partial amendment that changes only notice_days leaves option_rent at its last-set value. Building the full snapshot is a dictionary comprehension over the field set, and each entry keeps its own ResolvedField provenance for the audit trail.

Edge cases specific to commercial leases

  • Same effective and execution date. Two riders effective 2025-01-01, both executed 2025-01-01. The sequence tie-breaker picks a deterministic winner, but this is a genuine ambiguity of intent — flag it. Resolve it so pipelines don’t stall, and simultaneously emit a review signal so a human confirms the order.
  • Revert to base / null. An amendment sets cam_cap back to “as in the original lease.” Model it as REVERT_TO_BASE, not as value=None treated as “unknown.” The resolver restores the canonical base value and records reverted_to_base=True with the reverting amendment as provenance, so the audit trail shows why the field returned to base rather than looking like missing data.
  • Retroactive (backdated) amendment. An amendment signed 2025-06-01 but effective 2024-01-01 sorts before an amendment effective 2024-07-01. Because ordering keys on effective date, the retroactive change slots into the timeline where it belongs and does not automatically win just because it was signed most recently — the exact failure of “sort by signature date.”
  • Partial override of a compound field. A rider that changes only option_rent must not null the sibling notice_days. Field-level (sub-key-level) resolution keeps each sub-key on its own stream; never resolve compound structures as a single opaque blob.
  • Ambiguous execution date. Some amendments show only a month, or a range, or nothing legible. Do not coerce a guess into the sort key. If two events tie on effective date and one lacks a reliable execution date, the tie is unresolvable — hand it to review rather than letting the sequence fallback quietly decide real money.

When to escalate

The resolver is deterministic, but determinism is not the same as correct intent. Route a field to fallback routing logic for manual review when:

  • Two events tie on both effective and execution date. The sequence key prevents a crash, but a same-day, same-signature collision on a money field is a business ambiguity, not a data one — surface it.
  • A tie is broken only by an untrusted execution date. If the execution date that decided the winner was parsed with low confidence, the deterministic answer rests on a shaky input; escalate before it flows into escalation formula mapping.
  • An explicit supersession points at an amendment that isn’t in the ledger. A rider cancels “Amendment 3,” but no such event exists. Do not silently ignore the clause — the ledger is incomplete, and the resolver is resolving against a partial picture.
  • The winning value fails a downstream sanity gate. A resolved base_rent that jumps an order of magnitude may be a units error in a superseding amendment. Let the value resolve, but hold it for review before it posts.

Everything else resolves automatically and deterministically, with per-field provenance attached so any downstream number can be traced back to the exact amendment that set it.

Frequently asked questions

How do I decide which of two amendments to the same field wins? Order the field’s events by a three-part key: effective date first, then execution date, then a stable ingestion sequence. Fold the events in ascending order so the event with the greatest key is applied last and wins. The sequence component guarantees a deterministic winner even when the first two components tie exactly.

What is the difference between explicit supersession and implicit override? Implicit override is a later effective date silently out-ranking an earlier one during the fold. Explicit supersession is an amendment that names a prior amendment_id and voids it; that event is removed from the stream before folding, so it cannot influence the result even if its own key would otherwise rank highly.

How should the resolver handle an amendment that reverts a field to the original lease value? Model it as an explicit revert-to-base mode rather than a null or missing value. The resolver restores the canonical base value and records the reverting amendment as the provenance with a reverted-to-base flag, so the audit trail distinguishes a deliberate revert from absent data.

Should I resolve whole amendments or individual fields? Resolve individual fields. A single amendment rarely touches every term, so a document-level swap risks blanking fields the amendment never mentioned. Field-level resolution runs the winner logic per field against that field’s own event stream and keeps untouched fields at their last set value.

When should a conflict escalate to manual review instead of resolving automatically? Escalate when two events tie on both effective and execution date, when the deciding execution date was parsed with low confidence, when an explicit supersession references an amendment missing from the ledger, or when the resolved value fails a downstream sanity check. The resolver stays deterministic, but these cases are ambiguities of intent that a human should confirm.

← Back to Amendment Versioning & Ledgers

← Back to Amendment Versioning & Ledgers