Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Contributing

Explanation

Architecture boundaries

How three pytest files hold the module boundaries, the metadata-driven web rules, and the zero-finding lint and type-check state of Cluster ERP.

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.py and 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 subclass Event and 118 that subclass Query (counted with grep -c "^class .*(Event):" and grep -c "^class .*(Query):").
  • The guards door is imported 146 times under src/cluster_erp/modules (counted with grep -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 by products_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 src and tests only (.github/workflows/ci.yml:51); the scripts folder is checked only when pytest runs tests/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

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13