Agent-assisted authoring is recommended. Describe the architectural contract,
provide passing and failing examples, and ask a coding agent to implement and
test the rule. The result is ordinary Python that your team reviews and Fensu
executes like any other rule.
Agent-assisted authoring
Repository-specific static analysis traditionally required enough specialized work that architectural knowledge stayed trapped in documentation and code review. A coding agent can inspect the repository, implement a rule against Fensu’s public analysis API, add boundary tests, and iterate. The agent implements the policy; it does not invent it. Your team still defines the contract, reviews the rule and its edge cases, and decides which false positives are acceptable.Give the agent the contract
Start with examples rather than an implementation strategy. Give the agent enough context to inspect existing patterns and enough boundaries to distinguish the contract from nearby valid code:ctx.project, and diagnostics are deterministic.
The shape of a rule
A custom rule is a function decorated with@rule. The decorator carries a
mandatory metadata envelope; the function body is unconstrained.
The public API
Import rule-authoring names from the top-level package:fensu when
a custom rule needs it; do not import from fensu.analysis or another internal
module.
The top-level package also exports RuleCase, RuleFile, RuleResult, and
evaluate_rule for testing custom rules through the ordinary
discovery and evaluation pipeline.
The @rule decorator
Codes and namespaces
A custom code isX, plus zero or more uppercase letters, plus one or more
digits: X001, XAC001, XDB042. The namespace is validated when the decorator
runs; a custom code that starts with FF is rejected at load, so a project can
never silently redefine what a core rule means.
The letters between X and the digits are yours to carve into namespaces. A team
or plugin claims a prefix (XDB* for the database team, XDJ* for the Django
conventions package), and because selectors
are prefixes, namespaces nest: X selects every custom rule, XD everything
under XDB and XDJ, XDB one namespace, XDB001 one rule.
The check signature
The signature is frozen. New capabilities arrive as new context methods, never as signature changes.moduleis the parsedast.Moduleof the file being checked.ctxis aRuleContext.- The return value is a list of
Faultobjects.
The Fault
A Fault is a frozen dataclass. You usually construct one through the context so
line, column, and code are wired automatically, but the type is public so return
types are nameable.
Family, Severity, and Threshold
Threshold names the configurable numeric limits, one member per key in the
[thresholds] table (Threshold.MAX_STATEMENTS
is max_statements, and so on). It exists so a rule asks for a limit by name
rather than by string: ctx.threshold(name=Threshold.MAX_STATEMENTS) returns the
value applicable to the current file, with any global, per-role, or per-path
override already applied. There is no string-keyed form, for the same reason
FFH007 exists.
The rule context
RuleContext is a convenience toolbox, not a cage. Its supported analysis surface
is split into five public, backend-neutral zones. You may still ignore them and walk
module yourself when a rule needs unrestricted Python AST access.
ctx.project query records its input for cache invalidation. Pass the file
whose result depends on the query as requester, normally ctx.path. Construct
diagnostics through the context as well:
ctx.nodes()
over ast.walk(module) for module-wide scans so cooperating rules share one
traversal.
Where rules live
Keep project rules in arules/ role module inside your tooling tree. A rules/
module may contain imports, import-only TYPE_CHECKING blocks, and any number of
@rule functions; supporting logic belongs in the other roles.
fft104.py or xac001.py, because a
descriptive name remains useful when policies grow or codes change.
Loading rules
Point configuration at your rules, and select them:rule_pathsare directories (or files) of.pymodules. Each file is loaded under an isolated synthetic module name, so it cannot pollute or collide with your import namespace, and only the decorated functions that file defines are picked up.rule_modulesare importable dotted names, loaded with a normal import. Their source must resolve inside the repository.
X family selector turns on all
default-enabled custom rules; an exact code turns on a single rule (and is the only
way to enable an enabled_by_default=False custom rule).
Testing rules
Test a rule with the public harness rather than constructing internal objects. Each case runs one decorated rule through the same discovery, configuration, evaluation, and dependency tracking asfensu check. Import RuleCase, RuleResult,
evaluate_rule, and RuleFile from fensu.
expected_fault_count documents the case; evaluate_rule does not assert it for
you. RuleCase accepts path, scope (root, test, or tooling),
scope_root, supporting files, and a config fragment limited to contracts,
roles, rule_exceptions, threshold_overrides, and thresholds. Supporting
RuleFile entries are discovered as project context but are not direct evaluation
targets. RuleResult exposes faults, dependencies, and fault_count.
FFR707 requires each configured custom rule to have at least
min_custom_rule_test_cases statically visible RuleCase instances (default 1,
0 disables it). Keep cases in a visible pytest.mark.parametrize list calling
evaluate_rule(rule=<decorated function>, test_case=...); dynamic factories and
opaque collections cannot prove a count.
Keeping custom rules cacheable
fensu check caches results by default, but a custom rule
body is arbitrary Python, so Fensu cannot assume that it reads only inputs the
cache can track. By default, selecting any custom rule disables caching for the
whole run.
Setting require_cacheable = true in the
[cache] table changes this: every selected
custom rule is verified at startup, and the run stays cached. This tracked-input
isolation is sometimes called “hermeticity.” Verification requires the rule’s
source file to:
- Import only from a pure standard-library allowlist (
ast,re,dataclasses,typing,collections,enum,functools,itertools,pathlibfor lexical path work, and similar pure modules), fromfensuitself, and from the configured rule packages. - Never call
open,eval,exec,input, or__import__. - Never touch the filesystem directly. Read project state through the public
ctx.projectzone (exists,is_file,is_dir,directory_entries,glob,python_anchor,analysis, and focused semantic queries), which records inputs so cache invalidation remains correct.
Selecting and inspecting
Once configured, custom rules are indistinguishable from core rules to the rest of Fensu. They run underfensu check, they appear in
fensu rule (with their source file shown), and they are included in
the guidance produced by fensu skills:
message and remediation envelope is the same one core rules use,
your custom rules automatically become part of your repository’s generated agent
guidance, generated from the rules your CI enforces.
