Skip to content

symfonic.devtools.archgate.expiry

expiry

Exception expiry and registry hygiene.

The T1.2.4 registry already fails CI on an expired waiver. Three states remain green while the waiver is already worthless, and this module names them:

  • expiring soon — inside the horizon; someone must act before it bites.
  • overdue review — past its own review_date while still unexpired: the owner promised to look, and the date passed.
  • unused — it has suppression scope but suppresses nothing. The debt is gone; the licence to reintroduce it is not. Empty historical ratchet tombstones carry no such licence and are not unused waivers.

Expiry is fatal (expired-exception, raised by the registry itself). The other three are warnings by default and violations under --strict-hygiene, because an unused waiver is not a broken build — it is a broken promise.

build_expiry_report

build_expiry_report(entries: tuple[ExceptionEntry, ...], suppressed: tuple[tuple[Violation, str], ...] | list[tuple[Violation, str]], today: date | None = None, horizon_days: int = 90) -> ExpiryReport

Countdown, review status, and use count for every registered waiver.

Source code in src/symfonic/devtools/archgate/expiry.py
def build_expiry_report(
    entries: tuple[ExceptionEntry, ...],
    suppressed: tuple[tuple[Violation, str], ...] | list[tuple[Violation, str]],
    today: dt.date | None = None,
    horizon_days: int = 90,
) -> ExpiryReport:
    """Countdown, review status, and use count for every registered waiver."""
    today = today or dt.date.today()
    uses = Counter(entry_id for _, entry_id in suppressed)
    rows = [
        ExpiryRow(
            entry=entry,
            days_to_expiry=(entry.expiry - today).days,
            days_to_review=(entry.review_date - today).days,
            uses=uses.get(entry.id, 0),
        )
        for entry in entries
    ]
    rows.sort(key=lambda row: (row.days_to_expiry, row.days_to_review, row.entry.id))
    return ExpiryReport(rows=tuple(rows), horizon_days=horizon_days, today=today)

check_registry_hygiene

check_registry_hygiene(entries: tuple[ExceptionEntry, ...]) -> list[Violation]

Structural defects a waiver can carry while still parsing cleanly.

Source code in src/symfonic/devtools/archgate/expiry.py
def check_registry_hygiene(entries: tuple[ExceptionEntry, ...]) -> list[Violation]:
    """Structural defects a waiver can carry while still parsing cleanly."""
    violations: list[Violation] = []
    for entry in entries:
        problems: list[str] = []
        if entry.review_date > entry.expiry:
            problems.append(
                f"review_date {entry.review_date.isoformat()} is after expiry "
                f"{entry.expiry.isoformat()} — the entry can never be reviewed in time"
            )
        if len(entry.rationale.strip()) < MIN_RATIONALE_CHARS:
            problems.append(
                f"rationale is {len(entry.rationale.strip())} characters; a waiver must "
                f"explain the debt and its retirement in at least {MIN_RATIONALE_CHARS}"
            )
        if entry.rule in EDGE_SCOPED_RULES and not entry.edges:
            problems.append(
                f"rule {entry.rule} is about one import, but the entry names only "
                "modules; boundaries.py records these as modules=(importer,), so a "
                "module-level waiver would also pre-suppress every future edge that "
                "importer grows -- the count falls while nothing became enforceable. "
                "Declare edges = [\"importer -> imported\", ...] instead"
            )
        if entry.edges and entry.rule not in EDGE_SCOPED_RULES:
            problems.append(
                f"rule {entry.rule} carries no import edge, so the entry's "
                f"{len(entry.edges)} declared edge(s) can never match and the waiver "
                "would silently suppress nothing"
            )
        for problem in problems:
            violations.append(
                Violation(
                    rule="registry-hygiene",
                    message=f"exception {entry.id}: {problem}",
                    modules=(entry.id,),
                )
            )
    return violations

unused_violations

unused_violations(report: ExpiryReport) -> list[Violation]

--strict-hygiene: promote every unused waiver to a violation.

Source code in src/symfonic/devtools/archgate/expiry.py
def unused_violations(report: ExpiryReport) -> list[Violation]:
    """``--strict-hygiene``: promote every unused waiver to a violation."""
    return [
        Violation(
            rule="unused-exception",
            message=(
                f"exception {row.entry.id} (owner {row.entry.owner}, rule "
                f"{row.entry.rule}) suppresses nothing; remove it"
            ),
            modules=(row.entry.id,),
        )
        for row in report.unused
    ]