Cluster ERP holds its architecture rules in tests rather than in memory. This page explains the three test files that do it: tests/test_architecture_boundaries.py, tests/test_metadata_driven_rules.py, and tests/test_static_quality.py. It covers what each test reads, what it rejects, and what it does not see. The EventBus itself is explained on event bus.
In short¶
- No module may import another module. The single exception is
cluster_erp.modules.auth.guards(tests/test_architecture_boundaries.py:24,tests/test_architecture_boundaries.py:46-57). - Every module folder has a
module.pyand at least one HTTP route of its own (tests/test_architecture_boundaries.py:60-72). - The generic web engines do not compare against business module names (
tests/test_architecture_boundaries.py:75-87). - Rules that once lived in generic web code are now declared in module metadata, and tests check the metadata (
tests/test_metadata_driven_rules.py:1-5). - Ruff and mypy must report zero findings across the package, not only on changed files (
tests/test_static_quality.py:1-11).
How it works¶
The boundary test reads source files as text or syntax trees. It does not import the modules. The metadata test imports each module and calls get_metadata. The static test runs Ruff and mypy as subprocesses.
The flowchart shows which imports the boundary test allows for a file inside a module folder.
flowchart TD
file["Python file in src/cluster_erp/modules/X"] --> imp{"imports cluster_erp.modules.Y?"}
imp -->|no| ok1["allowed"]
imp -->|"Y equals X"| ok2["allowed"]
imp -->|"name equals DOOR"| ok3["allowed"]
imp -->|"any other Y"| fail["offender: path, line, import"]In prose: an import that does not name cluster_erp.modules.* passes. An import of the file's own module passes. An import of exactly cluster_erp.modules.auth.guards passes. Every other import of a module is listed as an offender with its path and line (tests/test_architecture_boundaries.py:48-56).
Module isolation¶
MODULE_NAMES is every folder under src/cluster_erp/modules/ that does not start with __ (tests/test_architecture_boundaries.py:25-27). _imports walks the syntax tree of each file and collects import and from ... import names with their line numbers (tests/test_architecture_boundaries.py:31-39). test_modules_exist guards against an empty scan: it requires at least 20 module folders (tests/test_architecture_boundaries.py:42-43).
Every module is an application¶
test_every_module_is_an_application_with_its_own_api first lists modules without a module.py. It then counts route decorators such as @router.get( in each module and lists modules with none (tests/test_architecture_boundaries.py:60-72).
Generic engines and business names¶
BUSINESS_MODULES is every module except auth, records, and logs (tests/test_architecture_boundaries.py:28). The test searches gen-list.js, gen-detail.js, gen-form.js, entity.js, and gen-settings.js for a comparison or case against one of those names (tests/test_architecture_boundaries.py:75-87).
Metadata-driven rules¶
tests/test_metadata_driven_rules.py loads every module's metadata through ModuleRegistry().discover() (tests/test_metadata_driven_rules.py:23-29). Its checks are:
| Test | Rule it holds | Source |
|---|---|---|
test_every_is_active_column_declares_the_active_flag_formatter |
An is_active list column declares the active_flag formatter |
tests/test_metadata_driven_rules.py:36-48 |
test_guided_party_creation_is_declared_in_metadata |
Party creation offers the roles customer, supplier, and employee from metadata |
tests/test_metadata_driven_rules.py:51-60 |
test_record_documents_provider_is_declared_by_capability |
Exactly one module provides record_documents, and it is documents |
tests/test_metadata_driven_rules.py:63-74 |
test_assistant_starters_are_module_metadata_in_both_languages |
Assistant starters carry English and Turkish labels | tests/test_metadata_driven_rules.py:77-82 |
test_generic_web_code_names_no_module_paths_permissions_or_language_columns |
Eleven generic web files name no /documents/ path, no document. permission, no label_tr or label_en column, and no language branch |
tests/test_metadata_driven_rules.py:85-112 |
test_home_screen_has_no_layout_rewrite_shims |
home.js holds no layout migration shims |
tests/test_metadata_driven_rules.py:115-119 |
test_ui01_rewrites_retired_tiles_and_stamps_the_layout |
The ui01 migration rewrites retired home tiles and is idempotent |
tests/test_metadata_driven_rules.py:122-155 |
test_home_widgets_are_module_metadata_rendered_by_module_bundles |
Home widgets are declared by modules and rendered by each module's gated bundle | tests/test_metadata_driven_rules.py:158-193 |
test_working_calendar_is_the_calendars_modules_custom_tab |
The working calendar tab is a custom tab owned by calendars |
tests/test_metadata_driven_rules.py:196-214 |
Static quality ratchet¶
test_ruff_is_zero_across_the_repo runs ruff check on src/, tests/, and scripts/ (tests/test_static_quality.py:26-35). test_mypy_is_zero_across_the_package runs mypy on src/cluster_erp (tests/test_static_quality.py:21-23, tests/test_static_quality.py:38-46). Both fail on any finding.
Why it is built this way¶
The module docstring of the boundary test says the rules are "held by a test instead of by memory" and that the guards door is "a platform dependency injection, not a business call" (tests/test_architecture_boundaries.py:1-7). The metadata test says that a regression that re-inlines a rule in generic code "fails here first" (tests/test_metadata_driven_rules.py:1-5). The static test says that a suite that only measures what changed "lets old debt grow back silently" (tests/test_static_quality.py:3-6).
Rules and boundaries¶
- Cross-module work goes through contracts in
src/cluster_erp/core/contracts.py. That file holds 146 classes that subclassEventand 118 that subclassQuery(counted withgrep -c "^class .*(Event):"andgrep -c "^class .*(Query):"). - The guards door is imported 146 times under
src/cluster_erp/modules(counted withgrep -rn "cluster_erp.modules.auth.guards"). - A module that needs another module's data asks over the bus. Example: product breakdown validation sends
ValidateProductBreakdown, handled byproducts_and_services(src/cluster_erp/core/product_breakdown.py:11-13,src/cluster_erp/modules/products_and_services/module.py:131).
Interaction with other parts¶
The application layer in src/cluster_erp/server/routes/ is outside the scanned folder. It may call several module services in one request, as party_composite.py states (src/cluster_erp/server/routes/party_composite.py:4-9). The decision record on EventBus-only communication gives the rule's context. The metadata-driven UI record covers the web rules.
Limits and open points¶
- The boundary test inspects Python imports only. A module that reaches another module's table through raw SQL is not detected. Not determined from code whether any module does so; no test searches for it.
- The generic engine test checks five files and only for comparison patterns against business module names (
tests/test_architecture_boundaries.py:76-78). - CI runs Ruff on
srcandtestsonly (.github/workflows/ci.yml:51); the scripts folder is checked only whenpytestrunstests/test_static_quality.py.
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| guards door | DOOR |
The one module path that every other module may import: cluster_erp.modules.auth.guards. |
Koruma kapısı | tests/test_architecture_boundaries.py:24 |
| business module | BUSINESS_MODULES |
Every module folder except auth, records, and logs, used by the generic engine test. |
İş modülü | tests/test_architecture_boundaries.py:28 |
| static quality ratchet | test_static_quality.py |
A pytest file that fails on any Ruff or mypy finding across the code base. | Statik kalite mandalı | tests/test_static_quality.py:1-11 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.