Skip to content

Concepts

Rule Families

The seven core rule families, native pack namespaces, and stable rule-code structure Fensu ships.

Every Fensu rule belongs to a family. A family groups rules by subject, gives you a single selector to turn the whole group on or off, and carries a letter in each rule’s code. Fensu ships seven core families, reserves FP for native rule packs, and reserves X for custom rules.

A core rule code is FF + a family letter + exactly three digits, for example FFL001 or FFS131.

  • FF marks a Fensu fault and is collision-free with other linters’ prefixes.
  • The third letter is the family.
  • The number is a stable address within the family. The hundreds digit groups a sub-theme (for example FFS0xx are size caps, FFS1xx are dataflow), and the rule’s slug carries the precise meaning.

Because codes are structured, any prefix is a valid selector: FF matches every core rule, FFS a family, FFS1 a hundreds bucket, FFS120 one rule.

Codes are permanent. A removed or renamed rule’s code is retired forever and never reused, and existing rules are never renumbered. Fensu-shipped native packs use FP plus a pack namespace, such as FPDG022 for Dagster. Retained core aliases preserve the family and number after that namespace, such as FPDGA001 for FFA001. Custom rules must use the X namespace (X + optional uppercase letters

  • digits, like XDB001) and are rejected at load if they start with FF or FP. See Native rule packs for shipped namespaces and Custom rules for how teams carve their own X sub-namespaces.
SelectorFamilySubject
FFLLayersImports, boundaries, dependency direction.
FFRRolesModule taxonomy: what a file may contain and where it lives.
FFSShapeFunction size caps and dataflow discipline.
FFNNamingName-to-behavior contracts.
FFHHygieneClarity and smell: docstrings, comments, raises, decision literals.
FFTTestsTest-suite layout, naming, and parametrization conventions.
FFAAnnotationsType-annotation completeness.

Which families apply depends on the file’s scope: product roots and tooling get the structural families (FFL, FFR, FFS, FFN, FFH, FFA), while the tests scope gets FFT and FFA.

Layers rules govern which modules and packages may import which, so boundaries stay intact. This is the family most directly aimed at keeping a codebase from tangling.

CodeSlugChecks
FFL001absolute-imports-onlyRelative imports hide package boundaries.
FFL002no-star-importsStar imports hide names from boundary analysis.
FFL101no-sibling-package-internalsDo not reach into a sibling owner’s internals; import its published surface.
FFL102no-cross-package-internalsImport only via a package’s public surface.
FFL103no-internal-public-surface-importsInternal code imports concrete owning modules, not the bare package surface.
FFL110no-cross-file-use-of-helper-private-classA _-prefixed class in _helpers/ is file-only; shared classes move to classes/.
FFL301no-runtime-imports-from-toolingRuntime code must not import from the tooling scope.

For FFL101 and FFL102, ownership is role-boundary-relative: a module’s owner is everything before its first role segment. Imports within one owner are free (main/ to its own _helpers/, between modules of one _helpers/ tree including its buckets, into the owner’s classes/). Imports across owners must enter through the owner’s public surface: its role files or role packages (models, types, constants, exceptions, classes) or its main/ entry modules. Another owner’s _helpers/ internals always fault. If an importable path has no role segment, Fensu infers its containing package as the owner (or the package itself for an __init__.py); a direct non-role module is internal, not a public surface.

The largest family, and the clearest separation from tools that only look at the import graph: FFR looks inside modules. It enforces what each role file may contain, where declarations may live, package layout, entry-module shape, and file size.

Representative rules:

CodeSlugChecks
FFR001models-only-modelsmodels.py holds only model declarations.
FFR101model-declaration-outside-modelsA model declared outside models.py.
FFR204banned-generic-package-nameNo shared, common, util, misc, and similar package names at any depth.
FFR301helpers-package-layout_helpers/ is flat or fully grouped into bounded buckets, never mixed. See containers.
FFR306top-level-domain-shapeA domain is a role leaf or a branch of named subdomains, never both. See leaf or branch.
FFR307top-level-direct-modulesRuntime roots and top-level domains do not contain ad hoc direct modules; deterministic root public facades are exempt.
FFR308shared-domain-prefixRepeated first-token domain prefixes become an owning branch domain. See shared prefixes.
FFR310public-facade-import-directionRuntime code imports a facade’s owning domain directly; facade imports are reserved for tests, tooling, and external consumers.
FFR401entry-module-shapeAn entry file defines one public function and at most two private ones (a limit on functions in the file, not on what they may call). See entry modules.
FFR402init-module-empty__init__.py is docstring-only.
FFR403no-reexport-shimInternal modules do not exist only to re-export imports; deterministic root public facades are exempt.
FFR406public-surface-shapeA root __init__.py contains only a docstring, imports, and at most one __all__ declaration.
FFR501classes-one-class-per-moduleOne class per module in classes/.
FFR601source-file-line-countA file must not exceed the line-count threshold.
FFR701tooling-entrypoint-shapeA direct tooling script is a thin command adapter.
FFR704rules-role-contentA rules/ module holds only imports and @rule functions.
FFR706descriptive-rule-module-namesRule modules use policy names, not filenames such as fft104.py or xac001.py.
FFR707custom-rule-test-coverageEvery configured custom rule has statically visible public-harness cases.

FFR spans role-file content (FFR0xx), misplaced declarations (FFR1xx), naming (FFR2xx), package layout (FFR3xx), surface shape (FFR4xx), class and helper shape (FFR5xx), file size (FFR6xx), and tooling layout (FFR7xx).

FFR307, FFR403, and FFR310 work together for a recognized Python root facade. The first two permit the bounded facade shape; the third prevents product runtime code from depending inward on that outward-facing compatibility surface. See Root public facades for the recognized shapes and import direction.

Shape rules keep functions small and their dataflow explicit. Several of them target the way generated code tends to sprawl.

CodeSlugChecks
FFS001too-many-statementsStatement cap on main/ orchestrator functions.
FFS002too-many-distinct-callsDistinct-callee cap on main/ functions.
FFS003too-many-localsAssigned-local cap on main/ functions.
FFS010max-argumentsGlobal argument-count cap on any function.
FFS011max-statements-globalLoose global statement floor on any function.
FFS101meaningful-project-result-discardedA meaningful returned value must be used, not silently dropped.
FFS110default-mutation-returnA function that mutates a parameter must return it.
FFS120keyword-only-argumentsParameters past the positional threshold must be keyword-only.
FFS130no-outer-state-mutationNo rebinding or mutation of module globals or closure captures.
FFS131no-complex-comprehensionsOne generator per comprehension, no nested comprehension.
FFS201mutable-result-modelmodels.py dataclasses must be frozen=True.

FFS102 (parameter-mutation-in-phase-helpers) is the one core rule that is opt-in: it ships disabled and runs only when named explicitly by code. It is the stricter input-immutability stance for teams adopting the full phase model, layered on top of the default FFS110.

Naming rules enforce that a name means what it claims. This is the “dishonest functions” family.

CodeSlugChecks
FFN001validator-must-not-returnvalidate_* and enforce_* functions must not return a meaningful value.
FFN002predicate-must-return-boolis_*, has_*, can_*, and supports_* functions must declare bool, TypeGuard, or TypeIs.
FFN003value-name-must-return-valueget_*, to_*, and as_* functions must not declare a no-value return such as None.
FFN004iterator-name-must-produce-iteratoriter_* functions must contain an owned yield or declare an iterator or generator return.

Each diagnostic offers two safe directions: honor the name or preserve the behavior and rename it. For example, an eager iter_items() -> list[Item] can return an iterator or become collect_items.

The shipped patterns and behaviors are configured through contracts, where projects can add their own name globs without changing these rule codes.

Hygiene rules target clarity and smells that hide meaning.

CodeSlugChecks
FFH001single-line-docstringsDocstrings are a single line.
FFH002no-standalone-commentsNo standalone comments the tooling would depend on.
FFH003no-raw-builtin-raiseRaise structured errors, not bare built-ins.
FFH004no-assert-in-runtimeNo assert in runtime code.
FFH005no-swallowed-exception-probeNo except that silently swallows.
FFH006no-complex-comprehensions-in-toolingThe comprehension readability limit, in tooling.
FFH007no-unnamed-string-decisionsString literals must not directly control comparisons.
FFH008no-magic-numeric-comparisonsNumeric literals other than -1, 0, 1 must not control comparisons.
FFH009no-import-time-side-effectsNo standalone calls executed at import time.

FFH007 and FFH008 push decisions to compare against named concepts (enum members or named constants) instead of unexplained literals. They report the literal expression and do not infer runtime values. FFH009 was previously numbered FFR206; import-time discipline is hygiene, not module taxonomy.

Test rules make a test suite readable and consistent. They apply only in the tests scope. The conventions include a mirrored layout, given-when-then naming, and dataclass-backed parametrization.

Representative rules:

CodeSlugChecks
FFT001test-layoutTest directories mirror the source tree.
FFT002test-scopeTest scopes come from the configured test_scopes.
FFT104no-if-in-testsTests and local test helpers contain no conditional control flow.
FFT204local-test-types-fileEach test module has a sibling _test_types.py.
FFT301test-file-nameTest files start with test_.
FFT302test-function-nametest_given_<state>_when_<action>_then_<expectation>.
FFT401dataclass-parametrizeTests use @pytest.mark.parametrize with a dataclass-backed case.
FFT414description-lambda-idsParametrize ids come from lambda case: case.description.

FFT groups layout and mirroring (FFT0xx), module hygiene (FFT1xx), test-role files (FFT2xx), test file and function naming (FFT3xx), and dataclass-backed parametrization (FFT4xx).

FFT104 applies to every test function, not only parametrized tests, and to top-level functions, async functions, and classes in helpers.py and _test_helpers.py. It detects if, match, conditional expressions, and filtered simple comprehensions. It does not currently treat while or try as conditionals. Use parametrization when setup and assertions stay branch-free. Otherwise split the behavior into separate tests or helper functions.

Annotation rules require types to be explicit, so they are in the text the reader (and a type checker, and an LLM) can see. FFA runs over both the product and test scopes.

CodeSlugChecks
FFA001parameter-annotationEvery function parameter is annotated.
FFA002return-annotationEvery function has a return annotation.
FFA101module-variable-annotationModule-level variables are annotated.
FFA102class-attribute-annotationClass attributes are annotated.
FFA103local-variable-annotationNon-scalar local variables are annotated on first binding.

All five are on by default. FFA103 exempts direct scalar literal bindings whose type is already explicit in the value, including strings, bytes, booleans, numeric literals, signed numeric literals, and f-strings:

attempts = 3
label = f"job-{job_id}"

Collections, constructor and function-call results, comprehensions, attribute reads, and other inferred values still require an annotation on first binding:

items: list[Item] = load_items()
by_id: dict[str, Item] = {item.id: item for item in items}

See Philosophy for why explicit annotations remain the default for those less obvious values.

Look up any rule’s full metadata, including its message and remediation, with fensu rule:

fensu rule FFS131

Generate agent-facing guidance from your active rules with fensu skills.