"""What a corpus has to be able to do to take part in the loop. The queue is generic; applying a decision is not. Only the support-KB code knows how to append a section to a support-KB article, and only the security answer library will know how to add an employee-voice alias to one of its entries. This is the seam between them. Registration is by import side effect (the corpus module calls :func:`register_adapter` at import time) rather than by a central list, so a corpus that is not loaded simply has no adapter and its proposals refuse to apply — which is the correct behaviour and a much better failure than a central list that silently gets out of date. """ from __future__ import annotations from typing import Any, Protocol, runtime_checkable from sqlalchemy.ext.asyncio import AsyncSession class ProposalApplyError(RuntimeError): """Applying failed for a reason the reviewer should read. Carries a ``superseded`` flag because "the page you were judging has since been retired" is not the same event as "the write blew up", and the queue shows them differently: one is nobody's fault and closes the row, the other is a defect and keeps it. """ def __init__(self, message: str, *, superseded: bool = False) -> None: super().__init__(message) self.superseded = superseded @runtime_checkable class CorpusAdapter(Protocol): """The corpus half of the loop.""" corpus: str async def apply( self, db: AsyncSession, *, proposal: Any, actor_person_id: Any, trace_id: str, ) -> dict[str, Any]: """Carry out an accepted proposal. Must be safe to call twice: the reviewer's click can be retried, and a failed apply is retried by hand. Returns a summary dict; a key named ``applied_ref`` is stored on the proposal. Raises :class:`ProposalApplyError` when the change cannot be made. """ ... async def describe( self, db: AsyncSession, *, proposal: Any, ) -> dict[str, Any]: """Everything the review screen needs to judge this proposal. For an edit that means the page as it stands today, so the reviewer is deciding against what is actually published rather than against what was published when the proposal was written. """ ... _ADAPTERS: dict[str, CorpusAdapter] = {} def register_adapter(adapter: CorpusAdapter) -> None: _ADAPTERS[adapter.corpus] = adapter def adapter_for(corpus: str) -> CorpusAdapter | None: return _ADAPTERS.get(corpus) def registered_corpora() -> list[str]: return sorted(_ADAPTERS) __all__ = [ "CorpusAdapter", "ProposalApplyError", "adapter_for", "register_adapter", "registered_corpora", ]