Scopes
A scope is a set of paths that receives a particular policy. Strata has three, each declared in configuration:| Scope | Config key | Policy |
|---|---|---|
| Product | roots | Full structural rules: layers, roles, shape, naming, hygiene, and annotations. |
| Tests | tests | Test-convention rules and annotation rules. Not the product structural rules. |
| Tooling | tooling | The same structural families as product roots, at one less nesting level. |
setup.py or noxfile.py, is simply skipped. There is no “scanned
but unclassified” state.
rootsis required and its entries must not be nested inside one another, so every scanned file belongs to exactly one root. Independent package trees in a monorepo are expressed as multiple roots.testsdefaults to["tests"].toolingdefaults to empty.
src/my_package, the file src/my_package/config/main/load.py has relative parts
("config", "main", "load.py"), from which Strata derives its domain, subdomain,
and role.
Roles
A role is what a file or directory is for. Strata recognizes a fixed set of role names and enforces what each may contain and where it may live.| Role | Form | Holds |
|---|---|---|
main/ | Package | Published entry surfaces and orchestrator functions. |
helpers/ | Package | Phase functions supporting the orchestrators. |
classes/ | Package | One class per module. |
models.py | File | Data model declarations (frozen dataclasses). |
types.py | File | Type declarations and aliases. |
constants.py | File | Constant declarations. |
exceptions.py | File | Exception class declarations. |
models.py may not live elsewhere,
and models.py may not hold unrelated code. Cross-boundary imports go through a
package’s published surface (its main/ entries and role files), never into its
internals.
Role names are fixed today. User-defined role taxonomies are a planned future
addition, not current behavior.
Domains and subdomains
Strata’s default layout for product code is domain, then subdomain, then role, not a flat top level.- The package root (for example
src/my_package/) holds only its__init__.pypublic surface and domain packages. No role files live at the root. - A domain (
src/my_package/config/) holds only__init__.pyand subdomains. No role files or loose modules live directly at the domain level. - A subdomain holds the actual work:
main/,helpers/,classes/, and the role files (models.py,types.py,constants.py,exceptions.py).
core/.
A domain with genuinely independent parts uses several named subdomains.
Vocabulary lives with its owner
Strata has no neutral “shared” or “utils” bucket. Every type lives in the role file of the subdomain that produces it, and other subdomains import it from there. A type that many places use is not ownerless; it has one producer, and that producer publishes it. If you cannot name the owner, that is a signal to look harder, not to create a junk drawer.Entry modules
A module directly under amain/ package is an entry module. Its shape rule
(SFR401) constrains only the module’s top level:
- exactly one public function (the entry point);
- at most two private functions in that file;
- top level contains only imports and function definitions, nothing else.
helpers/, classes from classes/, other subdomains’ published
surfaces, and anything else it has legitimate access to. The two-private-function
cap is not a cap on collaborators; it just says an entry module should not grow its
own pile of local glue. When you need more than a couple of local helpers, move them
into helpers/ and call them from there.
load_config calls four helper functions and several imported types. That is
entirely fine: it is one public function with zero private ones, and all the real
work lives in helpers/. The rule would only fire if the file grew a second public
function, a third private function, or a top-level statement that is not an import
or a def.
Default thresholds
Several shape rules are governed by numeric thresholds. Strata ships these defaults, all overridable in configuration:| Threshold | Default | Applies to |
|---|---|---|
max_statements | 40 | Statements in a main/ orchestrator function. |
max_distinct_calls | 20 | Distinct callees in a main/ function. |
max_locals | 20 | Assigned locals in a main/ function. |
max_statements_global | 70 | Statements in any function (loose global floor). |
max_arguments | 10 | Arguments to any function. |
max_positional_args | 0 | Positional (non keyword-only) parameters before * is required. |
max_file_lines | 2000 | Lines in a source file. |
max_flat_helper_modules | 10 | Flat modules in a helpers/ package before a subfolder is required. |
max_flat_main_modules | 20 | Flat entry modules in a main/ package. |
max_script_entrypoint_lines | 80 | Lines in a direct tooling script entrypoint. |
Tooling layout
Tooling code (declared intooling) follows the same roles as product code but at
one less nesting level, because dev scripts rarely need full domain depth.
- Direct
scripts/*.pyfiles are thin command adapters: one publicmain(), an optional_parse_args()or_build_parser(), imports, theif __name__ == "__main__"guard, and delegation to an importedmain/entry.scripts/__init__.pyis exempt. - A tool implementation uses one ownership level then a role:
main/, helpers/,
classes/, and rules/. A rules/ module is the home for custom rules; see
Custom rules.
Related
Configuration
Declare scopes, override thresholds, and set contracts.
Rule families
The rules that enforce this model.

