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 |
CountLineComment |
20 | warning | yes | Comment lines; on Python a docstring counts. A shrink signal |
LinesPerStatement |
4 | warning | yes | CountLineCode / CountStmt, judged from verbosity_min_statements = 5 statements. Synthetic. A shrink signal |
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) |
structure.call_cycle |
off by default | A cycle in the call graph among the routines the change reaches; set call_cycles to turn it on |
structure.reachable_complexity |
warning, off by default | A routine whose transitively reached CyclomaticStrict exceeds reachable_complexity |
structure.unused_routine |
warning, off by default | An affected routine nothing in the project calls or uses; set unused_routines to turn it on |
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.
The lean-code rules¶
Every limit above asks whether a piece of code is too complex. The nine rules of the
lean-code family ask whether it should exist at all. All nine carry the structure.
category, all nine ship off, and each is enabled by naming a severity on its switch in
[lean]. The keys and the excerpt init writes are in
Configuration; how to read a finding,
the three tags and the two the Gate never emits are on Lean code.
This section is one entry per rule: what it reports, what it deliberately does not, its
shipped default and the measurement behind it.
The switch and the rule are not spelled the same. The switch is named after the table it
configures (unused_parameters); the finding after what it reports, in the singular
(structure.unused_parameter).
Every measurement below names the task in .kiro/specs/lean-code-rules/research.md that took
it. Two repositories were measured on Understand 8.0 Build 1262 on 2026-09-11: this one,
319 analysed files, and facdrone, 945. Where an entry gives two numbers, they are in that
order. "First ten" means the first ten findings of the rule's output, read in the source and
sorted into genuine, same shape (twins an author keeps apart on purpose) and noise (nothing
an author would change).
Two floors in front of four rules¶
structure.unused_parameter, structure.unused_class, structure.unused_variable and
structure.pass_through make absence claims: nothing reads this, nothing else calls that.
Each sits behind two floors that ship set while every rule ships off:
resolution_floor = 0.75: the share of a language's call sites that resolved to a project routine. Below it the rule prints, once per run and per language,<rule> was not evaluated for <language>: 32% of this run's Python call sites resolved to a project routine, below the call-resolution floor of 75%, so a finding would report the analysis, not the code, and judges nothing in that language.accuracy_floor = 0.75: the share of filesund analyze -accuracyparsed with neither an error nor a warning, per side. Below it the rule prints, once per run,<rule> was not evaluated: Understand parsed 19% of this analysis without an error, below the accuracy floor of 75%; a name whose use sites sit in a region the analysis errored on reads as unused while it is read, and judges nothing.
A run that measured neither figure is refused the same way: an absent figure is not a good
one. Two floors because two failures were measured, and neither number bounds the other's
(task 3.3's review, task 6.4): on facdrone a naive "no reference, therefore dead" predicate
answered 830 routines at 26% accuracy, wrong nearly every time, because that codebase
satisfies its interfaces structurally and exactly one of the 830 carried an override
reference; on this repository at 19% accuracy every one of the sixteen src/ module bindings
the snapshot answered unreferenced was in fact read, its use sites sitting in regions the
analysis had errored on.
Both measured repositories are below both floors: accuracy 19.1% and 25.9%, call resolution 45.9% and 32.0% (6.4). On both, at the shipped floors, the four rules produce no finding by design, and the line they print instead is the whole of what the run says about dead code. It does not say there is none.
What the rules say below the floors was measured too, by forcing both to zero, so that an operator lowering a floor knows what they will get (6.3 first tens, 6.4 whole lists):
| Rule | Findings | What they are |
|---|---|---|
structure.unused_parameter |
68 and 58 | 56 and 39 are pytest fixture parameters of test_ routines, 4 and 16 are @overload stubs; first tens 0 and 2 genuine |
structure.pass_through |
47 and 59 | first tens 1 and 2 genuine forwarders, the rest one-line comprehensions and expressions holding a single project call |
structure.unused_variable |
17 and 55 | first tens 5 and 3 certain; facdrone's other seven need the file open, three of them named in other files the analysis did not resolve |
structure.unused_class |
0 and 18 | 8 of facdrone's first ten named in no other tracked source file |
Two of the four are mostly noise at any floor, and the noise is a routine shape no list of
names can spell: a test_ routine declaring a fixture it uses for its effect, an overload
stub whose body is ..., a comprehension that holds one call and forwards nothing. Those
exclusions belong to the rules and are recorded for their owners; until they land, enable
unused_parameters and pass_through last and read their first ten before trusting the
count. No repository above either floor has been measured, so nothing says at what figure the
variable rule's "named in other files" third vanishes. The floors move when one is measured,
not to license a run.
[lean] accuracy_floor is not [analysis] accuracy_floor. Both read the same figure; the
[analysis] one reports a poorly resolved run as a warning that never blocks and silences
nothing, the [lean] one refuses to judge. Lowering the reporting floor unlocks nothing
here.
structure.unused_parameter¶
Reports. A parameter of an affected routine that the routine never reads, sets or
modifies from a project file (Understand's Useby, Setby and Modifyby references), the
routine carrying no override reference. A defaulted parameter's own default is not a use:
Understand records no Set Init against it, so def advance(verbose=False) with verbose
unread is reported (6.1).
Does not report. A parameter of a routine that overrides another, because the signature
is the base's (req 1.4). A parameter of a method whose short name two or more project classes
declare, the interface-method tally of requirement 1.9: under structural typing an
implementation holds no reference to the interface it satisfies, so the tally stands in for
the inheritance edge. It is applied by short name, so a free function sharing a name with two
classes' methods is excused too; that false negative was measured at 3 and 2 routines against
the 33 and 157 interface methods the tally rightly excuses (6.4). Names on
unused_parameters_ignore, shipped as the receiver (self, cls, this), a leading
underscore and args/kwargs. Anything the change deleted: the after side is read, where a
deleted entity is absent (req 1.7). Anything at all below either floor, or when any affected
record was extracted before the rule was switched on (a stale warm cache), which is reported
as unavailable rather than judged in part.
Default. unused_parameters off; the ignore list as shipped.
Measurement. The whole lists at floors zero (6.4): 68 and 58 findings, of which 56 and 39
are pytest fixture parameters of test_ routines and 4 and 16 are @overload stubs; the 8
and 3 that remain include a dispatch-table signature and two parameters already carrying
# noqa: ARG001, the author and the rule agreeing. First tens 0 and 2 genuine (6.3). The
list is over parameter names and none of those shapes has one, so it was kept as shipped and
the routine-shape exclusion is recorded for the rule's owner.
structure.unused_class¶
Reports. An affected class that nothing in the project references: no use, set, modify, type, call or inheritance reference from a project file. Inheritance counts as use, so a base class whose only inbound reference is its one subclass's is used, not dead.
Does not report. Any class with a subclass, including an abstract base whose whole subtree
is dead; that is the cheaper error than reporting one class as both dead and worth folding
into its single implementation (task 1.6). Test*, *Error and *Exception
(unused_classes_ignore): pytest collects test classes by name and an exception is raised and
caught, neither of which is a reference the rule can see. Anything below either floor, or
deleted by the change.
Default. unused_classes off; the ignore list as shipped.
Measurement. Floors at zero: 0 of this repository's 338 classes, because every class in
src/ is named from a test; 18 of facdrone's 1 809, eight of the first ten named in no
other tracked source file and one re-exported from a package initialiser and used nowhere
(6.3, 6.4). Nothing in either list was an exception or a test class, so the list is neither
short nor long by measurement.
structure.unused_variable¶
Reports. An affected module-level binding that nothing in the project reads: no Useby,
Callby or Typedby reference from a project file. A write is not a use: a binding
something assigns and nothing reads is dead, which is also how Understand's own
CountUnusedVariable counts.
Does not report. Dunders, log, logger, pytestmark and _ (unused_variables_ignore):
read by the interpreter, the packaging tools, the logging machinery or pytest, or the
deliberate discard. Local variables, which are CountUnusedVariable's subject and not this
rule's. Anything below either floor.
Default. unused_variables off; the ignore list as shipped, ^_$ added in 6.4.
Measurement. This is the rule whose failure set the accuracy floor: at 19.1% every one of
the sixteen src/ bindings the snapshot answered unreferenced was in fact read (task 3.3's
review). Floors at zero: 17 here, first ten 5 genuine and 5 the _ discard, which is why
^_$ joined the list and not ^_, which would have excused facdrone's _SAMPLE_EVERY and
_MAX_LEVERAGE_STEPS, both bound and never mentioned again; 55 on facdrone, first ten 3
named nowhere else, 4 with a same-file mention and 3 named in other files the analysis did not
resolve, which is the accuracy floor's case exactly (6.3, 6.4).
structure.pass_through¶
Reports. An affected routine with exactly one project caller, exactly one project callee
and at most pass_through_max_statements = 2 statements, naming the callee. Understand's
CountStmt counts the def line, so 2 means a body of one statement, return other(x); a
guard clause would need 3 (6.4).
Does not report. One caller alone: a routine with one caller and a body of its own is
the decomposition this tool's own hints ask for (req 2.2). The caller's name: callers is a
count, and Python records a call made at module scope against the file rather than a
routine, so a finding naming "the only caller" could be wrong about the one thing it
asserted (task 4.2). Dunders, test_ routines, setup/teardown and main
(pass_through_ignore, the same four shapes as structure.unused_ignore). A routine whose
statement count was not taken, which is not judged rather than judged as empty; an operator
who deletes routine.CountStmt from the thresholds silences this rule without a note (6.1).
Anything below either floor. This rule's direction makes a partly resolved call graph worse
rather than quieter: an unresolved call site removes a caller, so a routine with three
callers of which two did not resolve measures one and qualifies (req 2.6).
Default. pass_through off; pass_through_max_statements = 2.
Measurement. Over the snapshots, 274 routines here and 459 on facdrone have one project caller, one project callee and no override; 56 and 86 are at 2 statements and none at 1, so 2 is requirement 2.2's line and the value was kept (6.4). Floors at zero: 47 and 59 findings, first tens 1 and 2 genuine forwarders, the rest one-line comprehensions, expressions around a call and constructor calls taking a call's result, each holding one project call edge in two statements exactly as a forwarder does (6.3). The number is not what separates those; the body-shape predicate is the rule's to tighten.
structure.single_implementation¶
Reports. A class with exactly one derived class and no project referrer other than that derived class, reported when the base or its one derived class is in the change, because the commit worth telling is usually the one that adds the second (req 3.2).
Does not report. *Error and *Exception (single_implementation_ignore): an exception
base with one subclass is what except Base catches, and collapsing it changes what callers
can catch. A base with two or more derived classes, or with any other referrer. It takes
no floor: referrers counts use, type and inheritance references rather than call edges,
so the resolution floor is not the bounding quantity; whether the accuracy floor should bound
it is an open question, because no corpus has paired an accuracy figure with a false-positive
count for this rule (task 4.2, 6.4).
Default. single_implementation off; the ignore list as shipped.
Measurement. 0 findings on both repositories, and both zeros are answers rather than refusals: 5 classes here and 12 on facdrone have exactly one derived class, and every one has between 3 and 144 other referrers (6.4). A rule that has produced no finding on two repositories cannot be paired with a false-positive rate, so no floor was added.
structure.over_export¶
Reports. An affected file that defines exactly one routine or class, holds nothing else at module level and is depended on by exactly one other file, naming the dependant, because the fix is to move the definition there and neither half is actionable without both names (req 4.1).
Does not report. A file nothing depends on: that is dead code and the dead-code rules'
subject. A file with two or more dependants: one definition with two users is the boundary
working. A file with a second module-level definition: a routine beside a module constant is
a module with state of its own. __init__.py, index.*, mod.rs and __main__.py
(over_export_ignore, path globs): four languages' idiom for holding one name for one
importer. A file whose declaration counts are missing, which is unmeasured rather than empty.
It takes no floor and has no unavailable state: it reads file metrics, file edges and the
definitions walk the snapshot already carries and asks for no reference on any entity. What it
does switch on is the definitions walk, so a snapshot cached without it is not reused.
Default. over_export off; the ignore list as shipped.
Measurement. On this repository two files define exactly one routine or class and neither
has exactly one dependant; on a 653-file snapshot twelve files qualified on the count and
exactly one, a factory module, was genuine (task 4.1). 0 and 1 on the 6.4 run, the one
genuine; a rule firing on all twelve would be a rule about file size, which
file.CountDeclFunction already is.
structure.duplicate_block¶
Reports. A run of at least duplicates_min_lines = 12 consecutive code lines in an
affected file that the project holds at another location, naming up to three other
locations. Whitespace, comment and newline tokens are dropped and the remaining lexemes of
each line joined and hashed, the normalisation Understand's own duplicate-lines plugin uses,
so the two agree on what a duplicate is. Both copies are reported when both are affected, and
two copies in one file count.
Does not report. A window shorter than the minimum, or a single repeated line: a match is
a 64-bit hash agreeing at min_lines aligned positions, and that margin is what lets a hash
comparison be reported as a fact about code. A run that repeats only itself, such as a table
of identical rows with a period below the minimum, because a reader has that range open
already and there is no separable copy to delete. Paths under duplicates_ignore. Lines of a
file the lexer refused, which is named in a note rather than read as a file with nothing in
it (req 5.8). It takes no floor: it makes a presence claim carried by the locations it
names, so low accuracy can make it quieter than the code deserves and cannot make it say
something false.
Default. duplicates off; duplicates_min_lines = 12; duplicates_ignore empty.
Measurement. 48 findings here, about 24 places since every block is reported from both
ends, and 152 on facdrone; first tens 8 and 5 genuine (6.3). The whole lists: 42 and 122 are
code, 6 and 30 are name lists, an __all__ or an import block of twelve or more names, which
the lexer sees as twelve code lines. A window of 15 would remove 4 and 17 of the lists at the
cost of 26 of the 42 and 63 of the 122 code findings, and a path pattern cannot name an
__all__, so 12 stays and the token-shape exclusion is recorded for the rule's owner (6.4).
Known and pinned rather than fixed: the three locations a finding names are three windows of
the one other copy, :352, :353, :354, so one copy reads as several places (6.2).
structure.similar_routine¶
Reports. One finding per family of routines whose token shapes match at
similar_threshold = 0.9 or above, identifiers and literals interchangeable, each member of
at least similar_min_statements = 6 statements, the family of at least
similar_min_family = 2, whenever an affected routine is a member. It names the other
members with their locations, the family's size and the weakest similarity holding it
together. A family within one file carries the shrink: hint (the /same_file variant);
across files, delete:.
Does not report. Pairs: on facdrone the 69 similar pairs at 0.9 are 44 families, and
sixty-six pairwise findings about twelve normalize methods is noise where one finding is a
task (req 5.9). A routine nested inside another as that routine's twin: a closure scores 0.95
against its parent and neither can be deleted in favour of the other, so the edge is refused
and a contained member is dropped from any family it reaches. A routine whose statement count
was not taken. Names on similar_name_ignore (dunders, setUp/tearDown and their
lower-case forms), which are neither reported, nor named as members, nor a link between two
families. Paths under similar_ignore. Cross-language pairs, which score 0.63 on the
contract fixture and form no family. No floor, on the same grounds as the block rule.
Default. similar_routines off; similar_threshold = 0.9, similar_min_statements = 6,
similar_min_family = 2; similar_ignore empty and similar_name_ignore as shipped.
Measurement. 163 families here and 129 on facdrone at the shipped numbers; first tens 6
and 7 genuine merges, the rest twins with the same skeleton over different facts kept apart
on purpose, and none noise, so every number was kept (6.3, 6.4). The pass is lossless: two
exact ceilings on how much two shapes can match refuse a pair before it is scored, and a
whole-project check --all with the rule on scores or refuses all 2 890 810 pairs of this
repository's 2 405 considered routines in 24.4 s (task 5.9).
structure.net_growth¶
Reports. A change whose net statement delta, the figure of the net line, exceeds
max_net_growth, at net_growth_severity = "warning".
Does not report. Anything unless an operator sets max_net_growth: the net line never
blocks on its own (req 7.5). 0 is a legal maximum and means the change may not make the
project longer. Nothing on --all, which has no before side and no delta.
Default. max_net_growth unset, so off; net_growth_severity = "warning".
Measurement. The figure itself: net: +12 lloc (+30 lines) over 7 routines is
CountStmt added minus removed, summed over the routines of the change's files on both
sides, with CountLineCode beside it; a routine that exists only before counts its whole
size negatively and one only after positively (req 7.2). Because the sum is over
routines, a deleted file that held no routines, a constants module or an __init__.py of
imports, reads as net: +0 lloc (+0 lines) over 0 routines: a genuine zero over nothing,
not a missing measurement, and the design's reading of requirement 7.2 rather than a defect
in the number. A routine whose signature changed is paired across the two sides by the
ratchet's own join, which one commit in five on this repository needs (task 2.2). The line and
its lean already: nothing to cut companion are on the CLI page.
The shrink metrics: length without complexity¶
routine.LinesPerStatement and routine.CountLineComment are the family's two rung-six
signals and are ordinary threshold rows in the routine table above, on by default as
warnings, ratcheted like any routine metric, and configurable under [thresholds.routine]
and [scope.*]. The one [lean] key they read is verbosity_min_statements = 5: below that
many statements the ratio is not judged, because a two-statement routine spread over six
lines scores 3.0 by arithmetic and would outrank a genuinely long one.
LinesPerStatement shipped at 3.0 and was raised to 4.0 in 6.4: everything read in the band
between them on both repositories was a single statement the formatter had wrapped one item
per line, and a ceiling whose findings are the formatter's is one an operator deletes. At 4.0
the tail is 13 of 3 467 judged routines here (0.4%) and 204 of 5 085 on facdrone (4.0%).
CountLineComment at 20 leaves 23 of 6 969 routines here and 11 of 9 450 there outside
(6.3). On Python a docstring counts as comment lines and not as code, which is why
file.RatioCommentToCode ships as a minimum only and no comment maximum on files ships at
all.
What reference-based detection cannot see¶
structure.unused_routine is documented as a warning because a reference database cannot
see an entry point named in packaging metadata, a handler a decorator registers, a dunder the
interpreter calls or a test pytest collects; unused_ignore excuses those four shapes out of
the box and the remedy is a pattern, never an added caller. The three dead-code rules above
inherit every one of those blind spots, since a parameter of an uncalled routine and a class
only a decorator names are unreferenced for the same reason, and add their own, which are per
language.
What the family reads was measured on the contract project (task 6.1, Build 1262): the two languages it builds, Python and C++, with every reference rule on, and a scratch tree in the six it does not, each with one base, one derived type and, where the language has one, an interface and its implementer. Inheritance is read on the base class, naming the derived one, in every language measured; the pair order in Understand's kind documentation varies per language and the direction does not.
| Language | Inheritance kind on the base | Override on the overriding method | Measured |
|---|---|---|---|
| Python | Python Inheritby |
Python Overrides |
contract project, every rule |
| C++ | C Public Derive |
C Overrides, with or without the override keyword |
contract project, every rule; Shape shape(side) is a C Callby Implicit and counts as a call |
| Ada | Ada Derive |
Ada Overrides |
scratch tree, kinds only |
| C# | c# csharp Derive; an interface c# csharp Implementby |
c# csharp Overrides |
scratch tree, kinds only |
| Fortran | Fortran Extendby |
none: Fortran records no override reference | scratch tree, kinds only |
| Java | Java Extendby Coupleby; an interface Java Implementby Coupleby |
Java Overrides |
scratch tree, kinds only |
| Pascal | Pascal Derive |
Pascal Overrides |
scratch tree, kinds only |
| TypeScript | web Javascript Extendby; an interface web Javascript Implementby |
web Javascript Overrides on extends; none on implements |
scratch tree, kinds only |
What that leaves unseen, per language:
- Python. A call made outside any routine is recorded against the file, so a routine
called once from a routine and once from module scope measures one caller and can satisfy
structure.pass_through.selfandclsare ordinary parameter entities and are excused by name, not by kind. Every parameter kind the five families declare is matched by the oneParameter ~Catchfilter. - C++. Each translation unit carries clang's libstdc++ note, which
-accuracycounts against it: the contract project measures 61.9% on 21 files and is itself refused by the accuracy floor, which a test asserts. A pure virtual, an interface method or a prototype whose definition is outside the analysis has no body, so it is absent from the token index and neither duplication rule can see it. - TypeScript. An
implementscarries no override reference on the implementing method, so for that shape requirement 1.4's exclusion rests on the interface-method tally alone. A plain.jsfile holding an ES6class ... extendsproduced no entities at all in a scratch probe: observed, not asserted. - Fortran. No override reference at all, so an overriding procedure's unread parameters
are reported like any other's, and the tally is the only guard. The extractor's class kinds
(
class,interface,struct) record no Fortran derived type, sostructure.unused_classandstructure.single_implementationnever reach one. - Ada. The same: a tagged type is not a class kind the extractor records, so the two class
rules do not run on it. A primitive operation declares a
Selfparameter of the type, and itsTypedbyreference sits outside the class's own members, so an Ada base with one operation would count a referrer and never be a single implementation. - Java, C#, Pascal. Inheritance and overrides are recorded as the table says and matched by the shipped kind set; nothing beyond one base, one derived type and one override was planted, so multiple inheritance, generics and nested types are unmeasured in every language.
- Not measured at all. Basic, Rust, VHDL and Objective-C: their
Implement,ExtendandObjC Extendkinds are read off the installed kind documentation, not off a database, and are unverified per language until one is built. The two shapes where the worker's caller count and Understand'sCountCallbyUniquewould part, a module-scope call and a caller outside the analysis root, did not occur in the fixture, so on it the two agree on all 36 routines and nothing says by how much they disagree elsewhere.
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.
analysis.accuracy¶
The other rule about the analysis rather than about the code. und analyze -accuracy reports
the share of files that parsed with neither an error nor a warning, and
analysis.accuracy_floor turns a figure below the floor into a finding.
It is a warning and it never blocks, whatever the severity map says, and that is a decision rather than a default. A poor figure is usually a third-party package Understand has no source for, an interpreter version it does not model, or a language feature it has not caught up with. None of those is fixable by the commit in front of you, and a gate that refused the commit over them is a gate that gets switched off.
Off by default: without a floor there is no finding, only the figure in the report and in the JSON. A build that reports no accuracy at all — 6.5, or 8.0 never asked — reports nothing rather than zero, because the absence of a measurement is not a measurement.
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.
One metric that was measured and not shipped¶
CRAP — comp^2 * (1 - cov)^3 + comp, over a ceiling of 30 — is the composite most often
proposed for this gate, and it is not among the rules above. The gate reads git and an
Understand database and has no coverage term, and with that term at zero the metric is
strictly increasing in comp: measured over 10 463 routines of one repository and 7 050 of
this one, it ranks both populations in exactly the order CyclomaticStrict ranks them, with
zero discordant pairs and an identical tie structure, and CRAP > 30 is CyclomaticStrict
≥ 6 under another name. Supplying real coverage points the rule the wrong way: at 80%
coverage it forgives every routine either repository's CyclomaticStrict ceiling refuses,
including one of 21 branches.
The measurement, the tables and what to run instead are in Against CodeQL, Semgrep, and the structure tools.
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.