Rules and defaults¶
Rule names¶
<scope>.<metric> routine.CyclomaticStrict, project.AVG:CountLineCode
structure.<rule> structure.file_cycle, structure.fan_out
codecheck.<check_id> a CodeCheck check id
analysis.<rule> analysis.parse_error -- the analysis itself failed
Scopes are routine, class, file, project and arch. The last two have no entities of
their own: a threshold on them is checked against the population of its scope and yields one
project-level finding with no entity and no path.
Threshold defaults¶
Every default has a limit. Not every default has a ratchet.
Routine¶
| Metric | Limit | Severity | Ratchet | What it counts |
|---|---|---|---|---|
CyclomaticStrict |
10 | error | yes | Decision points, counting && and \|\| |
CyclomaticModified |
8 | error | yes | Decision points, a switch counting once |
Essential |
4 | warning | yes | Unstructured control flow. Why it only warns |
MaxNesting |
3 | error | yes | Deepest nesting level |
CountLineCode |
60 | error | yes | Lines containing code |
CountStmt |
40 | error | yes | Statements |
CountParams |
5 | error | yes | Declared parameters. Synthetic — Understand's own is unset for every language |
CountPath |
100 | error | yes | Acyclic execution paths |
Class¶
| Metric | Limit | Severity | Ratchet |
|---|---|---|---|
CountDeclMethod |
20 | error | no |
CountDeclMethodNonStub |
15 | error | no |
CountDeclInstanceVariable |
10 | error | no |
MaxInheritanceTree |
4 | warning | yes |
CountClassDerived |
8 | error | no |
CountClassCoupled |
12 | error | no |
PercentLackOfCohesion |
70 | warning | yes |
CountDeclMethodNonStub is synthetic: CountDeclMethod - 2 * CountDeclPropertyAuto.
CountDeclPropertyAuto exists for C# alone, so on every other language the two numbers are
equal. PercentLackOfCohesion is unavailable for Python and is dropped, with a report, on a
Python-only project.
File¶
| Metric | Limit | Severity | Ratchet |
|---|---|---|---|
CountLineCode |
500 | error | no |
CountDeclFunction |
25 | error | no |
CountDeclClass |
3 | error | no |
MaxCyclomaticStrict |
10 | error | yes |
RatioCommentToCode |
min 0.1 | warning | yes |
Project¶
Reduced over the population of the scope.
| Metric | Limit | Severity |
|---|---|---|
AVG:CyclomaticStrict |
3 | error |
AVG:CountLineCode |
30 | error |
MaxCyclomaticStrict |
15 | error |
MaxNesting |
5 | error |
Available stats prefixes: AVG, MEDIAN, MEDIANHIGH, MEDIANLOW, MEDIANGROUPED,
MODE, STDEV, VARIANCE. STDEV and VARIANCE are the population forms.
The eight rules with the ratchet off¶
file.CountDeclFunction class.CountDeclMethod
file.CountDeclClass class.CountDeclMethodNonStub
file.CountLineCode class.CountDeclInstanceVariable
class.CountClassCoupled
class.CountClassDerived
Each of these counts the declarations, collaborators or lines of the container, and each
goes up when you split the container's contents — which is the remedy the gate's own hints
name. Ratcheting them would make the gate refuse the refactoring it just asked for. The
absolute limits are untouched: a file with 40 functions still fails file.CountDeclFunction
at 25.
Full reasoning and the measurements are on The ratchet.
class.MaxInheritanceTree is deliberately not in that list, even though extracting a
superclass raises it. No hint in the catalogue asks for another inheritance layer;
MaxInheritanceTree's own hint asks for one fewer.
Structural rules¶
| Rule | Default severity | What it reports |
|---|---|---|
structure.file_cycle |
error | A strongly connected component of two or more files in the after-side dependency graph, that is not contained in any before-side component |
structure.arch_cycle |
error | The same, between architecture nodes |
structure.layer |
error | A new edge that a declared layer rule does not allow |
structure.new_dependencies |
error | A file that gained more than max_new_dependencies_per_file distinct new targets (default 5), not counting targets that hold no code |
structure.coupling |
error | More references between two architecture nodes than a declared rule allows |
structure.fan_in |
warning | A file or class depended on by more than the limit |
structure.fan_out |
warning | A file or class depending on more than the limit, and any affected entity whose fan-out grew |
structure.duplicate_definition |
warning | A module-level name bound to the same value in more files than duplicate_definitions (off by default) |
codecheck.<id> |
warning | A finding from an Understand CodeCheck configuration, if one is named |
Fan defaults: file_fan_in 50, file_fan_out 20, class_fan_in 30, class_fan_out 12.
Fan-out is ratcheted; fan-in is not, because being used more is not a regression. An entity that grew and broke its limit yields both findings. A direction with no configured limit is switched off entirely, ratchet included.
A file that only just became readable¶
When the before side of a file could not be parsed and the after side can, every violation
in it is reported as pre-existing and does not block, with the sentence measured here
for the first time.
The code was there; only the measurement is new. Blocking would mean that converting a file so Understand can finally read it costs one blocking finding per routine it revealed — for code the commit did not write. Nobody pays that twice, so the file stays unmeasured forever, which is the outcome the rule exists to prevent.
The exemption is narrow. A file appears in the before side's parse errors only if it was there and was tried, so a file the change added is never in the set, and a violation introduced into a file that already parsed is untouched. What it does forgive is a new violation written into a file that also stopped parsing before — and the alternative was measured to make the fix impossible.
When two limits pull against each other, the file-level one yields¶
The routine limits ask for extraction — every hint for MaxNesting, CyclomaticStrict and
CyclomaticModified says to move a block into its own named routine. Doing that raises
file.CountDeclFunction and file.CountLineCode by construction.
Half of that tension is already handled: those counts, and six others, ship with the ratchet off, so a single extraction can never be refused for making its container bigger. The other half is not. The ceiling still refuses the twentieth extraction, and a file of twelve small named helpers is the outcome the routine limits are asking for.
So when the two disagree, raise the file-level limit rather than undoing the split. Measured
on this repository: every routine and class ceiling contains at least 99% of its population,
while 69 of 210 files were outside CountDeclFunction = 25 — the two rules disagreeing,
in numbers.
If your project is mid-cleanup, do not derive limits from it at all yet; see the timing precondition below.
A note on timing¶
recommend measures the shape a project has. Run it while a routine-level cleanup is in
flight and it bakes in the shape somebody is working to change — most of all
file.CountDeclFunction, which rises with every long routine that becomes several named
helpers. Wait until the splits stop.
Scattered definitions: one value, many files¶
structure.duplicate_definition reports a module-level name bound to the same value in
more files than the limit. It is off by default — set duplicate_definitions to turn it
on, because collecting the bindings costs one extra pass over the database.
[structure]
duplicate_definitions = 3
duplicate_definitions_severity = "warning"
duplicate_definitions_ignore = ["log", "logger", "pytestmark"]
What it is for¶
No limit in the tables above catches this. Every file involved is small, simple and reads perfectly well on its own; the cost lands on whoever has to change the value. Measured on a 770-file project:
| Binding | Files |
|---|---|
_HORIZON_DAYS = 20 |
15 — and a sixteenth file binds the same name to 5 |
PROJECT = Path(__file__).resolve().parents[2] |
14 — and six more use parents[1] |
FloatArray = NDArray[np.float64] |
12 |
_ZERO = Decimal("0") |
9 |
_ONE = Decimal("1") |
7 — and one file binds it to the float 1.0 |
The first row is the shape worth understanding. Changing the re-select horizon means finding fifteen files, one of which deliberately disagrees, and no amount of reading any one of them reveals either fact. That is the working-set problem in its purest form: the definition is distributed, so the change is too.
Why it keys on the value, not the name¶
A name repeated with a different value in each file is usually deliberate local vocabulary —
HELP in every subcommand module of this project, __all__ in every package. Reporting those
would bury the real finding under the idiom. A name repeated with the same value is a
decision that was copied instead of shared.
The count is over the whole project; the finding is reported against the affected files. So the commit that adds the sixteenth copy is told about the other fifteen, and a commit that touches none of them is told nothing.
Before you collect anything: are these one decision?¶
The rule sees that N files bind a name to the same text. It cannot see whether those copies are supposed to move together, and that is the question to answer first. Reported from a real cleanup pass on a 770-file project, where three groups were correctly not collapsed:
| Binding | Why it stays | |
|---|---|---|
_FACTOR_VERSION = 1 in 7 modules |
Each factor's own version. Collapsing them makes a bug the moment one is bumped. | |
T = TypeVar("T") in 6 modules |
A per-module type variable. Sharing one would be wrong. | |
AS_OF = ... in test modules |
Test scenarios that happen to pick the same date. |
What separates these from a real finding is not the value, it is intent: a name bound to a
per-module identity reads differently from a name bound to a threshold, and the rule
cannot tell them apart. So the first move on a finding is not "collapse it" but "decide
whether these are one decision". When they are not, the name goes in
duplicate_definitions_ignore — that is what the list is for, alongside the log /
pytestmark idioms.
The same pass collapsed six groups that were one decision — a type alias in 13 files, a re-select horizon in 15, a numeric floor in 5, a project root under three names in 29 test modules — so the rule's yield is real; it is the triage that needs a human or an agent.
The most valuable finding is the one you leave open¶
Reported from the same pass: MIN_ACTIVE named two different thresholds in one project —
3 in four modules, 5 in five others. The rule correctly reports two same-value groups rather
than one, which is exactly the _HORIZON_DAYS hazard above seen from the other side: one
name, two meanings, and grep answers with whichever it finds first. Unifying them is a
quantitative decision, not a refactor, so leaving it visible is the right call.
What it cannot see¶
- A binding whose initialiser the lexer cannot recover — an augmented assignment, a tuple unpacking, a bare annotation, a value that does not close within twelve lines — is skipped, never grouped. Two unreadable initialisers are not evidence that two definitions agree.
- Semantic equality.
Decimal("0")andDecimal(0)are different text and so different definitions. The rule under-reports; it does not guess. - A value that references another module-level name. The comparison is on text, so
FILES = tuple(sorted(SOURCES))reads as one definition in every module that writes it — even where eachSOURCESis a different dictionary. This is the rule's one over-reporting case, found by running it on this project, and it is whatduplicate_definitions_ignoreis for. Check the referenced names before you collect anything. - The per-module idiom.
log = logging.getLogger(__name__)andpytestmarkare written out in every module on purpose. Put them induplicate_definitions_ignore. That list is names rather than values deliberately: the similar-lookingPROJECT_ROOT = Path(__file__).resolve().parents[2]— which the same project also writes withparents[1]in six other files — is a real finding, not an idiom.
A target with no code in it is not a dependency¶
structure.new_dependencies skips any target whose CountLineCode is 0 — a package
initialiser an import merely traverses, rather than one it uses.
Measured on this repository: a new test module importing four things scored six
dependencies, two of which were src/scitools_hook/__init__.py and
src/scitools_hook/cli/__init__.py. The second is one line of docstring; Understand reports
CountLineCode 0, CountStmt 0 and no declaration of any kind for it. Counting it left a new
file in a nested package a real budget of two or three imports against a limit of five, which
made the rule refuse the ordinary act of adding a module with a test.
The test is no code, not named __init__.py: it is language-agnostic, and an initialiser
that re-exports an API has code and goes on counting. A file the analyser could not read is
never treated as empty — its metrics are absent rather than zero, and dropping its edges would
be a coupling the gate quietly stopped measuring.
A cycle that grew a member is reported, because {a, b, c} is no subset of {a, b} — the
change made it worse. Self-loops are excluded. Each cycle finding names its members and the
closing edges with their reference counts:
error structure.file_cycle
2 files form a dependency cycle that did not exist before the change: pricing/catalog.py,
pricing/rates.py; closed by pricing/catalog.py -> pricing/rates.py (3 refs),
pricing/rates.py -> pricing/catalog.py (3 refs)
In whole-project mode there is no before side, so every cycle is reported as an inventory and none of them is called new.
analysis.parse_error¶
A category of its own rather than a metric under file., because it is not a measurement:
there is no number, no limit and nothing for a baseline to hold.
Blocking, by default, for any file in the selection that Understand could not read. Files outside the selection — the interpreter's own standard library, say — are reported and do not block.
Its severity is not configurable. There is no key that turns it into a warning globally, and that is deliberate: a lever that silences it everywhere is a lever that turns the gate into one that certifies files it never read. The only escape is per file and requires a written reason, which the report then quotes on every run. See Configuration.
Two limits that were demoted to warnings¶
Both were shipped as errors, both were measured, and both were demoted because the number is not comparable across the entities it ranks, so neither can carry a refusal. Both keep their limits and both keep their ratchets. Only the ability to block was removed.
This is documented in detail because it is the most useful thing in this reference: it is a worked example of what to do when a metric turns out not to mean what you assumed.
routine.Essential ranks style, and contradicts its own hint¶
Measured on Understand 6.5.1204. One file, one database:
- The same six-way branch written as six guard clauses scores
Essential7. - Written as one
elifladder with a single exit, it scores 1. - Both score
CyclomaticStrict7.
One guard clause scores 1 and three score 4, so the shipped maximum of 4 fires on the fourth guard clause. Understand counts an early return as unstructured control flow.
And the hint catalogue answers a routine.Essential finding with "extract the block that is
jumped out of into a routine that returns early" — which raises the metric. A default
that blocks the refactoring the same tool recommends is not a default.
On this repository, 25 of 998 routines are over the limit and 824 of them score 1.
The blocking half of the concern is untouched and still ships as an error: CyclomaticStrict
10, CyclomaticModified 8, MaxNesting 3 and CountPath 100.
class.MaxInheritanceTree measures where a base class lives¶
Also measured on 6.5.1204. One fixture, one und, varying only which interpreter Understand
analysed with:
class Model(BaseModel)scores 5 when pydantic is on that interpreter'ssys.path.- The same unchanged line scores 1 when it is not.
Since the interpreter is now pinned — and it must be, because a metric must not depend on which libraries happen to sit beside the gate — third-party depth is invisible. What is still visible is the pure-Python standard library, and it is expensive:
| Base | MaxInheritanceTree |
|---|---|
class X(Protocol) |
5 |
| a subclass of that | 6 |
enum.Enum, abc.ABC |
4 |
an Exception subclass |
3 |
| its child | 4 |
io.StringIO (a C module) |
0 |
So one level of project inheritance costs anything from 0 to 5 depending on where its base
lives, and the metric is loudest where there is no hierarchy and silent where there is
one. All five findings the shipped limit raises on this repository's src/ are Protocol
declarations or one exception subclass. None is a hierarchy this project built.
Raising the number instead was measured and rejected. A limit of 6 clears the standard library's floor today, stably across CPython 3.11 through 3.14, but it calibrates a shipped constant against the standard library rather than against the code, and it leaves the inversion in place: a framework hierarchy four deep still reports 1.
The number stays honest about what Understand saw. The severity stops it deciding a commit.
What blocks¶
- Only
errorcan block. Awarningnever blocks, in any mode. - A pre-existing threshold error blocks only under
ratchet.strict = true. - A ratchet finding on an entity still inside its own limit after the change is demoted
to a warning by
ratchet.below_limit_severity, which ships as"warning". Growth that crosses a limit, or growth on an entity already over one, blocks. - A ratchet finding can never be
pre-existing— it exists precisely because the value just got worse. - Nothing is filtered out of the report. Warnings and pre-existing findings are printed and counted; they are simply not counted as blocking.
below_limit_severity is a ceiling and never a promotion: a rule you demoted to warning
stays a warning even when it is set to error.
Configuring a severity¶
There is no central severity map, and a top-level [severity] table is rejected at load.
Each rule family carries its own key; the table is in
Configuration. A threshold table accepts exactly
max, min, ratchet and severity, and Metric = false works only inside a
[scope.*] override.
Boundary conditions¶
- Threshold breaches are strict:
value > maxandvalue < min. A value exactly at the limit passes. - Ratchet comparisons are strict too. A value that did not move produces no finding.
- Coupling and fan rules are
>rules: a file exactly at its limit is allowed.