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.
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 recommended resolver
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
sequencetie-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_capback to “as in the original lease.” Model it asREVERT_TO_BASE, not asvalue=Nonetreated as “unknown.” The resolver restores the canonical base value and recordsreverted_to_base=Truewith 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_rentmust not null the siblingnotice_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
sequencefallback 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
sequencekey 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_rentthat 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.
Related
- Modeling Append-Only Lease Ledgers for Audit Trails — the append-only storage model that produces the event stream this resolver reads; storage there, resolution here.
- Amendment Versioning & Ledgers — the parent guide covering how amendments are versioned, tracked, and audited across a lease’s life.
- Lease Data Models — the canonical field definitions the resolved snapshot writes back into for downstream consumers.
- Fallback Routing Logic — where unresolvable ties and low-confidence tie-breaks divert for human review instead of auto-resolving.