I built a risk-prediction service for loan covenants, then generalised it so a new subject is a registration rather than a fork. Two more domains have since been added without touching the shared training or explanation libraries; what they did change was the seam they plug into, and that is the honest shape of generalising something that exists.
Pluggability is easy to assert and the assertion is usually worth nothing, so here is the version git can check. Weather and esports each implement one protocol and register. Covenant, being first, is served by the service directly and is not itself a plug-in.
| Domain added | Shared libraries touched | Plug-in seam touched |
|---|---|---|
| weather | 0 files | DomainProtocol reshaped (37 lines added, 62 removed) and the generic worker's call into it (4 added, 6 removed) |
| esports | 0 files | the registry moved from instances to factories (40 added, 17 removed) and the entry point with it (38 added, 17 removed) |
So the training and explanation code never learned that weather or esports exist. The protocol did: the second domain is where decode-and-extract became one call the domain owns, and the third is where construction was deferred so a deployment running only esports never loads weather's fitted state. That is what the second and third implementations are for.
class DomainProtocol(Protocol):
"""Protocol that all domain implementations must satisfy.
A domain provides event codecs, feature extraction, alert context
generation, and Kafka topic configuration. The GenericStreamingWorker
delegates all domain-specific logic through this protocol.
"""
services/covenant-radar-api/src/covenant_radar_api/domains/protocols.py, the protocol both plug-ins implement.
Covenant compliance is exact rule evaluation: formulas, thresholds, frequencies. Breach risk is the learned path. They are separate code paths on purpose.
Conflating them would make a compliance answer probabilistic, which is worse than useless. A lender asking whether a borrower is in breach needs an exact answer from the contract terms, not a confidence score. The temptation is real because one model could nominally produce both.
The same idea runs through the model layer and the explanation layer. Twelve backends sit behind one interface, including gradient boosting I wrote from scratch competing against the production libraries in the same harness, and they are selectable per request.
Explainability follows the model rather than the endpoint: permutation importance for anything, SHAP tree explanations for tree models, plain and integrated gradients for the neural ones. One contract, three strategies, chosen by what is actually loaded.
Training on 40 real public credit, loan, default and bankruptcy datasets rather than generated data is the unglamorous half. Synthetic data is the usual shortcut, and it flatters everything.
Every figure above is bound to a named section of the wiki page
covenant-radar and checked mechanically before this page can
deploy. That page records how each count is reproduced and three ways of
getting it wrong, including one met while re-checking for this article.
Known limits. No accuracy figures are claimed here: this is a statement about architecture, not about how well the models score. The headline has been corrected twice. The source page first said all three domains were plug-ins, and two are. Then, until 2026-09-14, this page said the two additions touched the shared machinery "not at all"; the commits that added them show the protocol, worker and registry edited, and only the libraries untouched, so it now says that.
BackendName in libs/covenant_ml/src/covenant_ml/types.py; the 5 regressors: RegressorBackendName in types_regression.py.