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(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(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(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
]
|