Skip to content

symfonic.services.conversation.horizon

horizon

Resumability horizons: how long legacy-format state stays migratable.

Two horizons, because there are two deployments and they cannot share a clock:

  • :class:CalendarHorizon โ€” the operated platform. The operator publishes a cutoff date, notifies tenants inside a notice window, and after the cutoff legacy-format state expires. A calendar is meaningful because there is an operator who ships upgrades on it.
  • :class:PackageVersionHorizon โ€” library mode. An adopter's local state has no operator and no shared calendar; an artifact written in 2020 is still perfectly valid if the adopter is still inside the supported package version range. Keying library-mode expiry on a date would expire state that nothing has actually stopped supporting.

Both answer the same three-valued question, and every negative answer carries a reason and a support route. An expiry an adopter cannot act on is indistinguishable from a bug.

CalendarHorizon dataclass

CalendarHorizon(cutoff: datetime, notice_period: timedelta, support_route: str)

Operated-platform horizon: a published cutoff and a notice window.

HorizonDecision dataclass

HorizonDecision(verdict: HorizonVerdict, reason: str, support_route: str)

The horizon's answer for one artifact.

PackageVersionHorizon dataclass

PackageVersionHorizon(supported_through: str, support_route: str)

Library-mode horizon: keyed by package version, never by the calendar.

ResumabilityHorizon

Bases: Protocol

The shape both horizons share, so callers bind to neither.

decide

decide(ref: CheckpointRef, **context: object) -> HorizonDecision

Answer resumable / migration_required / expired for one artifact.

Source code in src/symfonic/services/conversation/horizon.py
def decide(self, ref: CheckpointRef, **context: object) -> HorizonDecision:
    """Answer resumable / migration_required / expired for one artifact."""

parse_version

parse_version(raw: str) -> tuple[int, int, int]

Parse major.minor.patch, tolerating a pre-release suffix.

Refuses anything it cannot read rather than sorting it low: a version that silently compares as 0.0.0 would place every unparseable build inside every support window, which is the wrong direction to fail.

Source code in src/symfonic/services/conversation/horizon.py
def parse_version(raw: str) -> tuple[int, int, int]:
    """Parse ``major.minor.patch``, tolerating a pre-release suffix.

    Refuses anything it cannot read rather than sorting it low: a version that
    silently compares as ``0.0.0`` would place every unparseable build inside
    every support window, which is the wrong direction to fail.
    """
    parts = raw.strip().split(".")
    if len(parts) < 2:
        raise ValueError(f"unparseable package version: {raw!r}")
    numbers: list[int] = []
    for part in parts[:3]:
        match = _VERSION_PART.match(part)
        if match is None:
            raise ValueError(f"unparseable package version: {raw!r}")
        numbers.append(int(match.group(1)))
    while len(numbers) < 3:
        numbers.append(0)
    return numbers[0], numbers[1], numbers[2]