Core-Agnosticism & Extension Seams
The core never imports a plugin. Plugins extend it only through the seams the core deliberately exposes: ports, registries and DI.
The rule
Core resolves standalone. No core module imports
from plugins.*; no core migration is anchored on a plugin; the core knows nothing about “subscription”, “shop” or any other vertical vocabulary.This is enforced by an AST oracle (tests/unit/test_core_agnosticism.py) that walks vbwd/ and fails the build on any plugin import, plus a vocabulary oracle that bans domain words leaking into core.
A port may live in core only if core routes through it
The litmus test: a port (interface) may live in core only if the core ROUTES through it. If core would have to READ a domain field, that's a leak and the concept belongs in a plugin. Money flows out of core as bus events and line-item handlers, never as direct calls into a vertical.
The seams plugins use
- Ports / interfaces — e.g.
ISubscriptionLifecycle,IWithdrawableBalance; a plugin implements them, core depends only on the abstraction. - Registries — the line-item handler registry, the checkout-source registry, the data-exchange registry, the entity-type registry, the deletion-dependency registry, fe-admin extension registries.
- DI providers — a plugin's
on_enableadds its repositories to the container (providers.Factory(Repo, session=container.db_session)); core declares none.
Example: extending the payment line items
# In the plugin's on_enable:
from vbwd.services.line_items import register_line_item_handler
register_line_item_handler("subscription", my_handler)
# core's PaymentCapturedHandler dispatches to the registered
# handler by item type — it never imports the plugin.The same shape powers data exchange, checkout sources and the fe-admin injection seams: core iterates a registry; plugins fill it.